summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/jetpack/modules')
-rw-r--r--plugins/jetpack/modules/after-the-deadline.php6
-rw-r--r--plugins/jetpack/modules/blocks.php6
-rw-r--r--plugins/jetpack/modules/calypsoify/README.md4
-rw-r--r--plugins/jetpack/modules/calypsoify/class.jetpack-calypsoify.php462
-rw-r--r--plugins/jetpack/modules/calypsoify/gutenberg-styles/button.scss143
-rw-r--r--plugins/jetpack/modules/calypsoify/mods-gutenberg.js28
-rw-r--r--plugins/jetpack/modules/calypsoify/mods.js89
-rw-r--r--plugins/jetpack/modules/calypsoify/style-gutenberg-rtl.min.css4
-rw-r--r--plugins/jetpack/modules/calypsoify/style-gutenberg.min.css2
-rw-r--r--plugins/jetpack/modules/calypsoify/style-rtl.min.css4
-rw-r--r--plugins/jetpack/modules/calypsoify/style.min.css2
-rw-r--r--plugins/jetpack/modules/carousel.php16
-rw-r--r--plugins/jetpack/modules/carousel/images/arrows-2x.pngbin0 -> 10063 bytes
-rw-r--r--plugins/jetpack/modules/carousel/images/arrows.pngbin0 -> 4529 bytes
-rw-r--r--plugins/jetpack/modules/carousel/images/carousel-likereblog-2x.pngbin0 -> 1096 bytes
-rw-r--r--plugins/jetpack/modules/carousel/images/carousel-likereblog.pngbin0 -> 547 bytes
-rw-r--r--plugins/jetpack/modules/carousel/images/carousel-link-2x.pngbin0 -> 867 bytes
-rw-r--r--plugins/jetpack/modules/carousel/images/carousel-link.pngbin0 -> 431 bytes
-rw-r--r--plugins/jetpack/modules/carousel/images/carousel-sprite-2x.pngbin0 -> 2076 bytes
-rw-r--r--plugins/jetpack/modules/carousel/images/carousel-sprite.pngbin0 -> 1318 bytes
-rw-r--r--plugins/jetpack/modules/carousel/jetpack-carousel-rtl.css1
-rw-r--r--plugins/jetpack/modules/carousel/jetpack-carousel.css1129
-rw-r--r--plugins/jetpack/modules/carousel/jetpack-carousel.js1851
-rw-r--r--plugins/jetpack/modules/carousel/jetpack-carousel.php832
-rw-r--r--plugins/jetpack/modules/carousel/rtl/jetpack-carousel-rtl.css1130
-rw-r--r--plugins/jetpack/modules/comment-likes.php205
-rw-r--r--plugins/jetpack/modules/comment-likes/admin-style.css45
-rw-r--r--plugins/jetpack/modules/comment-likes/comment-like-count.js42
-rw-r--r--plugins/jetpack/modules/comments.php36
-rw-r--r--plugins/jetpack/modules/comments/admin.php210
-rw-r--r--plugins/jetpack/modules/comments/base.php308
-rw-r--r--plugins/jetpack/modules/comments/comments.php626
-rw-r--r--plugins/jetpack/modules/contact-form.php29
-rw-r--r--plugins/jetpack/modules/contact-form/admin.php894
-rw-r--r--plugins/jetpack/modules/contact-form/class-grunion-contact-form-endpoint.php52
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-inline-editing-style-rtl.css825
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-inline-editing-style-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-inline-editing-style.css764
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-inline-editing-style.min.css2
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-style-rtl.css613
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-style-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-style.css554
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-style.min.css2
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-ui-rtl.css27
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-ui-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-ui.css26
-rw-r--r--plugins/jetpack/modules/contact-form/css/editor-ui.min.css2
-rw-r--r--plugins/jetpack/modules/contact-form/css/grunion-rtl.css1
-rw-r--r--plugins/jetpack/modules/contact-form/css/grunion.css91
-rw-r--r--plugins/jetpack/modules/contact-form/css/jquery-ui-datepicker.css160
-rw-r--r--plugins/jetpack/modules/contact-form/grunion-contact-form.php3501
-rw-r--r--plugins/jetpack/modules/contact-form/grunion-editor-view.php299
-rw-r--r--plugins/jetpack/modules/contact-form/grunion-form-view.php266
-rw-r--r--plugins/jetpack/modules/contact-form/images/blank-screen-akismet.pngbin0 -> 2270 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/blank-screen-button.pngbin0 -> 1823 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/grunion-form-2x.pngbin0 -> 153 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/grunion-form.pngbin0 -> 188 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/grunion-remove-field-2x.pngbin0 -> 201 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/grunion-remove-field-hover-2x.pngbin0 -> 207 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/grunion-remove-field-hover.gifbin0 -> 144 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/grunion-remove-field.gifbin0 -> 139 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/grunion-remove-option-2x.pngbin0 -> 99 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/grunion-remove-option-hover-2x.pngbin0 -> 94 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/grunion-remove-option-hover.gifbin0 -> 73 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/images/grunion-remove-option.gifbin0 -> 73 bytes
-rw-r--r--plugins/jetpack/modules/contact-form/js/editor-view.js284
-rw-r--r--plugins/jetpack/modules/contact-form/js/grunion-admin.js30
-rw-r--r--plugins/jetpack/modules/contact-form/js/grunion-frontend.js3
-rw-r--r--plugins/jetpack/modules/contact-form/js/grunion.js1180
-rw-r--r--plugins/jetpack/modules/contact-form/js/tinymce-plugin-form-button.js37
-rw-r--r--plugins/jetpack/modules/copy-post.php338
-rw-r--r--plugins/jetpack/modules/custom-content-types.php47
-rw-r--r--plugins/jetpack/modules/custom-css.php68
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/class.csstidy.php1246
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/class.csstidy_ctype.php46
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/class.csstidy_optimise.php938
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/class.csstidy_print.php410
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/cssparse-rtl.css119
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/cssparse-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/cssparse.css118
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/cssparse.min.css2
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/cssparsed-rtl.css30
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/cssparsed-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/cssparsed.css29
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/cssparsed.min.css2
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/data-wp.inc.php102
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/data.inc.php693
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/lang.inc.php308
-rw-r--r--plugins/jetpack/modules/custom-css/csstidy/wordpress-standard.tpl10
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css-4.7.php1165
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css.php1866
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/blank.css1
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/codemirror-rtl.css262
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/codemirror-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/codemirror.css262
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/codemirror.min.css2
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/css-editor-rtl.css33
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/css-editor-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/css-editor.css32
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/css-editor.min.css2
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/customizer-control.css150
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/rtl/codemirror-rtl.css260
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror-rtl.css7
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror.css6
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror.min.css2
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/js/codemirror.min.js11
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css-preview.js42
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js85
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.js192
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/js/css-editor.js91
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/js/use-codemirror.js52
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/preprocessors.php58
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/preprocessors/lessc.inc.php3768
-rw-r--r--plugins/jetpack/modules/custom-css/custom-css/preprocessors/scss.inc.php4383
-rw-r--r--plugins/jetpack/modules/custom-css/migrate-to-core.php243
-rw-r--r--plugins/jetpack/modules/custom-post-types/comics.php533
-rw-r--r--plugins/jetpack/modules/custom-post-types/comics/admin.css7
-rw-r--r--plugins/jetpack/modules/custom-post-types/comics/comics-rtl.css31
-rw-r--r--plugins/jetpack/modules/custom-post-types/comics/comics-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/custom-post-types/comics/comics.css30
-rw-r--r--plugins/jetpack/modules/custom-post-types/comics/comics.js134
-rw-r--r--plugins/jetpack/modules/custom-post-types/comics/comics.min.css2
-rw-r--r--plugins/jetpack/modules/custom-post-types/comics/rtl/comics-rtl.css32
-rw-r--r--plugins/jetpack/modules/custom-post-types/css/edit-items.css24
-rw-r--r--plugins/jetpack/modules/custom-post-types/css/many-items.css14
-rw-r--r--plugins/jetpack/modules/custom-post-types/css/nova-font.css30
-rw-r--r--plugins/jetpack/modules/custom-post-types/css/nova.css110
-rw-r--r--plugins/jetpack/modules/custom-post-types/css/portfolio-shortcode.css131
-rw-r--r--plugins/jetpack/modules/custom-post-types/css/testimonial-shortcode.css102
-rw-r--r--plugins/jetpack/modules/custom-post-types/js/many-items.js116
-rw-r--r--plugins/jetpack/modules/custom-post-types/js/menu-checkboxes.js55
-rw-r--r--plugins/jetpack/modules/custom-post-types/js/nova-drag-drop.js67
-rw-r--r--plugins/jetpack/modules/custom-post-types/nova.php1298
-rw-r--r--plugins/jetpack/modules/custom-post-types/portfolios.php929
-rw-r--r--plugins/jetpack/modules/custom-post-types/testimonial.php763
-rw-r--r--plugins/jetpack/modules/debug.php6
-rw-r--r--plugins/jetpack/modules/enhanced-distribution.php67
-rw-r--r--plugins/jetpack/modules/geo-location.php81
-rw-r--r--plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php425
-rw-r--r--plugins/jetpack/modules/google-analytics.php15
-rw-r--r--plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-legacy.php256
-rw-r--r--plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-options.php70
-rw-r--r--plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-universal.php410
-rw-r--r--plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-utils.php63
-rw-r--r--plugins/jetpack/modules/google-analytics/wp-google-analytics.php76
-rw-r--r--plugins/jetpack/modules/gplus-authorship.php6
-rw-r--r--plugins/jetpack/modules/gravatar-hovercards.php300
-rw-r--r--plugins/jetpack/modules/holiday-snow.php6
-rw-r--r--plugins/jetpack/modules/infinite-scroll.php247
-rw-r--r--plugins/jetpack/modules/infinite-scroll/infinity.css164
-rw-r--r--plugins/jetpack/modules/infinite-scroll/infinity.js780
-rw-r--r--plugins/jetpack/modules/infinite-scroll/infinity.php1664
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.css45
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.php44
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen-rtl.css216
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen.css216
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen.php28
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.css111
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.php48
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen-rtl.css168
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen.css168
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen.php58
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentysixteen-rtl.css161
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentysixteen.css161
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentysixteen.php43
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyten.css25
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentyten.php55
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentythirteen.css90
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentythirteen.php28
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentytwelve.css33
-rw-r--r--plugins/jetpack/modules/infinite-scroll/themes/twentytwelve.php43
-rw-r--r--plugins/jetpack/modules/json-api.php12
-rw-r--r--plugins/jetpack/modules/latex.php122
-rw-r--r--plugins/jetpack/modules/lazy-images.php40
-rw-r--r--plugins/jetpack/modules/lazy-images/images/1x1.trans.gifbin0 -> 42 bytes
-rw-r--r--plugins/jetpack/modules/lazy-images/js/lazy-images.js870
-rw-r--r--plugins/jetpack/modules/lazy-images/lazy-images.php353
-rw-r--r--plugins/jetpack/modules/likes.php657
-rw-r--r--plugins/jetpack/modules/likes/jetpack-likes-master-iframe.php38
-rw-r--r--plugins/jetpack/modules/likes/jetpack-likes-settings.php729
-rw-r--r--plugins/jetpack/modules/likes/post-count-jetpack.js20
-rw-r--r--plugins/jetpack/modules/likes/post-count.js64
-rw-r--r--plugins/jetpack/modules/likes/queuehandler.js402
-rw-r--r--plugins/jetpack/modules/likes/style.css233
-rw-r--r--plugins/jetpack/modules/manage.php4
-rw-r--r--plugins/jetpack/modules/manage/activate-admin.php4
-rw-r--r--plugins/jetpack/modules/manage/confirm-admin.php4
-rw-r--r--plugins/jetpack/modules/markdown.php29
-rw-r--r--plugins/jetpack/modules/markdown/easy-markdown.php812
-rw-r--r--plugins/jetpack/modules/masterbar.php26
-rw-r--r--plugins/jetpack/modules/masterbar/masterbar.php470
-rw-r--r--plugins/jetpack/modules/masterbar/overrides.css61
-rw-r--r--plugins/jetpack/modules/masterbar/rtl-admin-bar.php55
-rw-r--r--plugins/jetpack/modules/masterbar/tracks-events.js110
-rw-r--r--plugins/jetpack/modules/memberships/class-jetpack-memberships.php272
-rw-r--r--plugins/jetpack/modules/minileven.php57
-rw-r--r--plugins/jetpack/modules/minileven/images/wp-app-devices.pngbin0 -> 1014 bytes
-rw-r--r--plugins/jetpack/modules/minileven/minileven.php344
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/comments.php52
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/content-gallery.php83
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/content.php63
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/footer.php61
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/functions.php273
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/header.php70
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/image.php108
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/inc/custom-header.php107
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.eotbin0 -> 7475 bytes
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.svg81
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.ttfbin0 -> 13516 bytes
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.woffbin0 -> 8676 bytes
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/inc/template-tags.php96
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/inc/tweaks.php103
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/index.php75
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/js/small-menu.js38
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/page.php47
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/rtl.css574
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/screenshot.pngbin0 -> 58138 bytes
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/searchform.php12
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/sidebar.php12
-rw-r--r--plugins/jetpack/modules/minileven/theme/pub/minileven/style.css1588
-rw-r--r--plugins/jetpack/modules/mobile-push.php6
-rw-r--r--plugins/jetpack/modules/module-extras.php85
-rw-r--r--plugins/jetpack/modules/module-headings.php342
-rw-r--r--plugins/jetpack/modules/module-info.php914
-rw-r--r--plugins/jetpack/modules/monitor.php148
-rw-r--r--plugins/jetpack/modules/notes.php198
-rw-r--r--plugins/jetpack/modules/omnisearch.php6
-rw-r--r--plugins/jetpack/modules/photon-cdn.php298
-rw-r--r--plugins/jetpack/modules/photon-cdn/jetpack-manifest.php418
-rw-r--r--plugins/jetpack/modules/photon.php22
-rw-r--r--plugins/jetpack/modules/photon/photon.js61
-rw-r--r--plugins/jetpack/modules/plugin-search.php602
-rw-r--r--plugins/jetpack/modules/plugin-search/plugin-search.css84
-rw-r--r--plugins/jetpack/modules/plugin-search/plugin-search.js273
-rw-r--r--plugins/jetpack/modules/plugin-search/psh-128.pngbin0 -> 12524 bytes
-rw-r--r--plugins/jetpack/modules/plugin-search/psh-256.pngbin0 -> 27512 bytes
-rw-r--r--plugins/jetpack/modules/plugin-search/psh.svg1
-rw-r--r--plugins/jetpack/modules/post-by-email.php202
-rw-r--r--plugins/jetpack/modules/post-by-email/post-by-email-rtl.css7
-rw-r--r--plugins/jetpack/modules/post-by-email/post-by-email-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/post-by-email/post-by-email.css6
-rw-r--r--plugins/jetpack/modules/post-by-email/post-by-email.js128
-rw-r--r--plugins/jetpack/modules/post-by-email/post-by-email.min.css2
-rw-r--r--plugins/jetpack/modules/protect.php882
-rw-r--r--plugins/jetpack/modules/protect/blocked-login-page.php611
-rw-r--r--plugins/jetpack/modules/protect/math-fallback.php158
-rw-r--r--plugins/jetpack/modules/protect/protect-dashboard-widget-rtl.css117
-rw-r--r--plugins/jetpack/modules/protect/protect-dashboard-widget-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/protect/protect-dashboard-widget.css116
-rw-r--r--plugins/jetpack/modules/protect/protect-dashboard-widget.min.css2
-rw-r--r--plugins/jetpack/modules/protect/protect.pngbin0 -> 14439 bytes
-rw-r--r--plugins/jetpack/modules/protect/shared-functions.php316
-rw-r--r--plugins/jetpack/modules/protect/transient-cleanup.php58
-rw-r--r--plugins/jetpack/modules/publicize.php74
-rw-r--r--plugins/jetpack/modules/publicize/enhanced-open-graph.php129
-rw-r--r--plugins/jetpack/modules/publicize/publicize-jetpack.php753
-rw-r--r--plugins/jetpack/modules/publicize/publicize.php1249
-rw-r--r--plugins/jetpack/modules/publicize/ui.php657
-rw-r--r--plugins/jetpack/modules/pwa.php42
-rw-r--r--plugins/jetpack/modules/pwa/class.jetpack-pwa-helpers.php62
-rw-r--r--plugins/jetpack/modules/pwa/class.jetpack-pwa-manifest.php95
-rw-r--r--plugins/jetpack/modules/pwa/images/wp-192.pngbin0 -> 6584 bytes
-rw-r--r--plugins/jetpack/modules/pwa/images/wp-512.pngbin0 -> 16496 bytes
-rw-r--r--plugins/jetpack/modules/random-redirect.php6
-rw-r--r--plugins/jetpack/modules/related-posts.php63
-rw-r--r--plugins/jetpack/modules/related-posts/class.related-posts-customize.php300
-rw-r--r--plugins/jetpack/modules/related-posts/jetpack-related-posts.php1804
-rw-r--r--plugins/jetpack/modules/related-posts/related-posts-customizer.js28
-rw-r--r--plugins/jetpack/modules/related-posts/related-posts-rtl.css1
-rw-r--r--plugins/jetpack/modules/related-posts/related-posts.css315
-rw-r--r--plugins/jetpack/modules/related-posts/related-posts.js331
-rw-r--r--plugins/jetpack/modules/related-posts/rtl/related-posts-rtl.css190
-rw-r--r--plugins/jetpack/modules/search.php18
-rw-r--r--plugins/jetpack/modules/search/class.jetpack-search-helpers.php699
-rw-r--r--plugins/jetpack/modules/search/class.jetpack-search-template-tags.php225
-rw-r--r--plugins/jetpack/modules/search/class.jetpack-search.php1874
-rw-r--r--plugins/jetpack/modules/seo-tools.php39
-rw-r--r--plugins/jetpack/modules/seo-tools/jetpack-seo-posts.php63
-rw-r--r--plugins/jetpack/modules/seo-tools/jetpack-seo-titles.php301
-rw-r--r--plugins/jetpack/modules/seo-tools/jetpack-seo-utils.php126
-rw-r--r--plugins/jetpack/modules/seo-tools/jetpack-seo.php206
-rw-r--r--plugins/jetpack/modules/sharedaddy.php44
-rw-r--r--plugins/jetpack/modules/sharedaddy/admin-sharing-rtl.css453
-rw-r--r--plugins/jetpack/modules/sharedaddy/admin-sharing-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/sharedaddy/admin-sharing.css452
-rw-r--r--plugins/jetpack/modules/sharedaddy/admin-sharing.js532
-rw-r--r--plugins/jetpack/modules/sharedaddy/admin-sharing.min.css2
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/after-the-deadline@2x.pngbin0 -> 1068 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/comments@2x.pngbin0 -> 763 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/contact-form@2x.pngbin0 -> 539 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/custom.pngbin0 -> 445 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/custom@2x.pngbin0 -> 1147 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/designfloat.pngbin0 -> 833 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/digg.pngbin0 -> 530 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/digg@2x.pngbin0 -> 872 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/divider.pngbin0 -> 94 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/divider@2x.pngbin0 -> 116 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/draggy.pngbin0 -> 107 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/draggy@2x.pngbin0 -> 109 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/email.pngbin0 -> 209 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/email@2x.pngbin0 -> 927 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/ember.pngbin0 -> 533 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/enhanced-distribution@2x.pngbin0 -> 757 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/facebook.pngbin0 -> 568 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/facebook@2x.pngbin0 -> 1036 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/feed.pngbin0 -> 761 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/icon-facebook-2x.pngbin0 -> 1027 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/icon-facebook.pngbin0 -> 581 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/icon-twitter-2x.pngbin0 -> 1414 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/icon-twitter.pngbin0 -> 523 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/icon-wordpress-2x.pngbin0 -> 592 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/icon-wordpress.pngbin0 -> 666 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/kindle.pngbin0 -> 750 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/kindle@2x.pngbin0 -> 1505 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/linkedin-horizontal.pngbin0 -> 2115 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/linkedin-horizontal@2x.pngbin0 -> 2975 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/linkedin-nocount.pngbin0 -> 1564 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/linkedin-nocount@2x.pngbin0 -> 1736 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/linkedin-smart.pngbin0 -> 2115 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/linkedin-smart@2x.pngbin0 -> 2975 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/linkedin-vertical.pngbin0 -> 2274 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/linkedin-vertical@2x.pngbin0 -> 2653 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/linkedin.pngbin0 -> 360 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/linkedin@2x.pngbin0 -> 944 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/loading.gifbin0 -> 2530 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/more.pngbin0 -> 285 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/more@2x.pngbin0 -> 798 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/pinterest.pngbin0 -> 624 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/pinterest@2x.pngbin0 -> 1310 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/pocket.pngbin0 -> 367 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/pocket@2x.pngbin0 -> 504 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/print.pngbin0 -> 209 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/print@2x.pngbin0 -> 1052 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/reddit.pngbin0 -> 881 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/reddit@2x.pngbin0 -> 1500 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/rss.pngbin0 -> 870 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/rss@2x.pngbin0 -> 1775 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/share-bg.pngbin0 -> 82 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/sharing-hidden.pngbin0 -> 213 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/sharing-hidden@2x.pngbin0 -> 106 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-digg.pngbin0 -> 793 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-digg@2x.pngbin0 -> 1147 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-facebook.pngbin0 -> 1427 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-facebook@2x.pngbin0 -> 830 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-like.pngbin0 -> 1620 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-like@2x.pngbin0 -> 3800 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-pinterest.pngbin0 -> 1235 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-pinterest@2x.pngbin0 -> 2170 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-pocket.pngbin0 -> 641 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-pocket@2x.pngbin0 -> 1272 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-reddit.pngbin0 -> 1572 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-reddit@2x.pngbin0 -> 2601 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-skype.pngbin0 -> 1688 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-skype@2x.pngbin0 -> 4019 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-stumbleupon.pngbin0 -> 552 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-stumbleupon@2x.pngbin0 -> 2072 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-tumblr.pngbin0 -> 1531 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-tumblr@2x.pngbin0 -> 4179 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-twitter.pngbin0 -> 1952 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/smart-twitter@2x.pngbin0 -> 1691 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/tumblr.pngbin0 -> 742 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/tumblr@2x.pngbin0 -> 926 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/twitter.pngbin0 -> 523 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/twitter@2x.pngbin0 -> 1302 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/wordpress.pngbin0 -> 667 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/images/wordpress@2x.pngbin0 -> 1344 bytes
-rw-r--r--plugins/jetpack/modules/sharedaddy/recaptcha.php188
-rw-r--r--plugins/jetpack/modules/sharedaddy/sharedaddy.php289
-rw-r--r--plugins/jetpack/modules/sharedaddy/sharing-service.php955
-rw-r--r--plugins/jetpack/modules/sharedaddy/sharing-sources.php1793
-rw-r--r--plugins/jetpack/modules/sharedaddy/sharing.css755
-rw-r--r--plugins/jetpack/modules/sharedaddy/sharing.js514
-rw-r--r--plugins/jetpack/modules/sharedaddy/sharing.php638
-rw-r--r--plugins/jetpack/modules/shortcodes.php196
-rw-r--r--plugins/jetpack/modules/shortcodes/archiveorg-book.php130
-rw-r--r--plugins/jetpack/modules/shortcodes/archiveorg.php160
-rw-r--r--plugins/jetpack/modules/shortcodes/archives.php82
-rw-r--r--plugins/jetpack/modules/shortcodes/bandcamp.php243
-rw-r--r--plugins/jetpack/modules/shortcodes/brightcove.php295
-rw-r--r--plugins/jetpack/modules/shortcodes/cartodb.php21
-rw-r--r--plugins/jetpack/modules/shortcodes/class.filter-embedded-html-objects.php400
-rw-r--r--plugins/jetpack/modules/shortcodes/codepen.php11
-rw-r--r--plugins/jetpack/modules/shortcodes/crowdsignal.php610
-rw-r--r--plugins/jetpack/modules/shortcodes/css/quiz.css56
-rw-r--r--plugins/jetpack/modules/shortcodes/css/recipes-print-rtl.css1
-rw-r--r--plugins/jetpack/modules/shortcodes/css/recipes-print-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/shortcodes/css/recipes-print.css36
-rw-r--r--plugins/jetpack/modules/shortcodes/css/recipes-print.min.css2
-rw-r--r--plugins/jetpack/modules/shortcodes/css/recipes-rtl.css1
-rw-r--r--plugins/jetpack/modules/shortcodes/css/recipes-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/shortcodes/css/recipes.css36
-rw-r--r--plugins/jetpack/modules/shortcodes/css/recipes.min.css2
-rw-r--r--plugins/jetpack/modules/shortcodes/css/slideshow-shortcode-rtl.css145
-rw-r--r--plugins/jetpack/modules/shortcodes/css/slideshow-shortcode-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/shortcodes/css/slideshow-shortcode.css157
-rw-r--r--plugins/jetpack/modules/shortcodes/css/slideshow-shortcode.min.css2
-rw-r--r--plugins/jetpack/modules/shortcodes/css/style.css188
-rw-r--r--plugins/jetpack/modules/shortcodes/dailymotion.php357
-rw-r--r--plugins/jetpack/modules/shortcodes/facebook.php106
-rw-r--r--plugins/jetpack/modules/shortcodes/flatio.php12
-rw-r--r--plugins/jetpack/modules/shortcodes/flickr.php239
-rw-r--r--plugins/jetpack/modules/shortcodes/getty.php222
-rw-r--r--plugins/jetpack/modules/shortcodes/gist.php224
-rw-r--r--plugins/jetpack/modules/shortcodes/googleapps.php255
-rw-r--r--plugins/jetpack/modules/shortcodes/googlemaps.php132
-rw-r--r--plugins/jetpack/modules/shortcodes/googleplus.php37
-rw-r--r--plugins/jetpack/modules/shortcodes/gravatar.php163
-rw-r--r--plugins/jetpack/modules/shortcodes/houzz.php34
-rw-r--r--plugins/jetpack/modules/shortcodes/hulu.php275
-rw-r--r--plugins/jetpack/modules/shortcodes/images/collapse.pngbin0 -> 2072 bytes
-rw-r--r--plugins/jetpack/modules/shortcodes/images/expand.pngbin0 -> 2039 bytes
-rw-r--r--plugins/jetpack/modules/shortcodes/images/slide-nav.pngbin0 -> 5704 bytes
-rw-r--r--plugins/jetpack/modules/shortcodes/img/slideshow-controls-2x.pngbin0 -> 1944 bytes
-rw-r--r--plugins/jetpack/modules/shortcodes/img/slideshow-controls.pngbin0 -> 1009 bytes
-rw-r--r--plugins/jetpack/modules/shortcodes/img/slideshow-loader.gifbin0 -> 13545 bytes
-rw-r--r--plugins/jetpack/modules/shortcodes/instagram.php265
-rw-r--r--plugins/jetpack/modules/shortcodes/js/brightcove.js29
-rw-r--r--plugins/jetpack/modules/shortcodes/js/gist.js27
-rw-r--r--plugins/jetpack/modules/shortcodes/js/instagram.js25
-rw-r--r--plugins/jetpack/modules/shortcodes/js/jmpress.js2897
-rw-r--r--plugins/jetpack/modules/shortcodes/js/jquery.cycle.min.js9
-rw-r--r--plugins/jetpack/modules/shortcodes/js/main.js258
-rw-r--r--plugins/jetpack/modules/shortcodes/js/quiz.js67
-rw-r--r--plugins/jetpack/modules/shortcodes/js/recipes-printthis.js294
-rw-r--r--plugins/jetpack/modules/shortcodes/js/recipes.js16
-rw-r--r--plugins/jetpack/modules/shortcodes/js/slideshow-shortcode.js205
-rw-r--r--plugins/jetpack/modules/shortcodes/kickstarter.php80
-rw-r--r--plugins/jetpack/modules/shortcodes/mailchimp.php226
-rw-r--r--plugins/jetpack/modules/shortcodes/medium.php71
-rw-r--r--plugins/jetpack/modules/shortcodes/mixcloud.php75
-rw-r--r--plugins/jetpack/modules/shortcodes/pinterest.php50
-rw-r--r--plugins/jetpack/modules/shortcodes/polldaddy.php4
-rw-r--r--plugins/jetpack/modules/shortcodes/presentations.php465
-rw-r--r--plugins/jetpack/modules/shortcodes/quiz.php309
-rw-r--r--plugins/jetpack/modules/shortcodes/recipe.php515
-rw-r--r--plugins/jetpack/modules/shortcodes/scribd.php62
-rw-r--r--plugins/jetpack/modules/shortcodes/sitemap.php30
-rw-r--r--plugins/jetpack/modules/shortcodes/slideshare.php121
-rw-r--r--plugins/jetpack/modules/shortcodes/slideshow.php316
-rw-r--r--plugins/jetpack/modules/shortcodes/soundcloud.php327
-rw-r--r--plugins/jetpack/modules/shortcodes/spotify.php99
-rw-r--r--plugins/jetpack/modules/shortcodes/ted.php78
-rw-r--r--plugins/jetpack/modules/shortcodes/tweet.php276
-rw-r--r--plugins/jetpack/modules/shortcodes/twitchtv.php74
-rw-r--r--plugins/jetpack/modules/shortcodes/twitter-timeline.php56
-rw-r--r--plugins/jetpack/modules/shortcodes/unavailable.php81
-rw-r--r--plugins/jetpack/modules/shortcodes/untappd-menu.php69
-rw-r--r--plugins/jetpack/modules/shortcodes/upcoming-events.php36
-rw-r--r--plugins/jetpack/modules/shortcodes/ustream.php135
-rw-r--r--plugins/jetpack/modules/shortcodes/videopress.php20
-rw-r--r--plugins/jetpack/modules/shortcodes/vimeo.php301
-rw-r--r--plugins/jetpack/modules/shortcodes/vine.php68
-rw-r--r--plugins/jetpack/modules/shortcodes/vr.php148
-rw-r--r--plugins/jetpack/modules/shortcodes/wordads.php64
-rw-r--r--plugins/jetpack/modules/shortcodes/wufoo.php89
-rw-r--r--plugins/jetpack/modules/shortcodes/youtube.php396
-rw-r--r--plugins/jetpack/modules/shortlinks.php140
-rw-r--r--plugins/jetpack/modules/simple-payments/paypal-express-checkout.js248
-rw-r--r--plugins/jetpack/modules/simple-payments/simple-payments.css129
-rw-r--r--plugins/jetpack/modules/simple-payments/simple-payments.php683
-rw-r--r--plugins/jetpack/modules/site-icon.php6
-rw-r--r--plugins/jetpack/modules/site-icon/site-icon-functions.php29
-rw-r--r--plugins/jetpack/modules/sitemaps.php42
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-fallback.php146
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-image-fallback.php56
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-image.php68
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-master-fallback.php38
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-master.php44
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-news-fallback.php56
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-news.php68
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-page-fallback.php55
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-page.php67
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-video-fallback.php56
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer-video.php68
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-buffer.php325
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-builder.php1468
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-constants.php216
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-finder.php116
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-librarian.php431
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-logger.php86
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-state.php144
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemap-stylist.php783
-rw-r--r--plugins/jetpack/modules/sitemaps/sitemaps.php578
-rw-r--r--plugins/jetpack/modules/social-links.php6
-rw-r--r--plugins/jetpack/modules/sso.php1138
-rw-r--r--plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php325
-rw-r--r--plugins/jetpack/modules/sso/class.jetpack-sso-notices.php203
-rw-r--r--plugins/jetpack/modules/sso/jetpack-sso-login-rtl.css177
-rw-r--r--plugins/jetpack/modules/sso/jetpack-sso-login-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/sso/jetpack-sso-login.css176
-rw-r--r--plugins/jetpack/modules/sso/jetpack-sso-login.js32
-rw-r--r--plugins/jetpack/modules/sso/jetpack-sso-login.min.css2
-rw-r--r--plugins/jetpack/modules/stats.php1751
-rw-r--r--plugins/jetpack/modules/subscriptions.php781
-rw-r--r--plugins/jetpack/modules/subscriptions/readme.md12
-rw-r--r--plugins/jetpack/modules/subscriptions/subscriptions.css29
-rw-r--r--plugins/jetpack/modules/subscriptions/views.php755
-rw-r--r--plugins/jetpack/modules/theme-tools.php70
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentyfifteen-rtl.css744
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentyfifteen.css769
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentyfifteen.php35
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentyfourteen-rtl.css370
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentyfourteen.css365
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentyfourteen.php73
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentynineteen-rtl.css1
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentynineteen.css374
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentynineteen.php126
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentyseventeen.php13
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentysixteen-rtl.css832
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentysixteen.css827
-rw-r--r--plugins/jetpack/modules/theme-tools/compat/twentysixteen.php68
-rw-r--r--plugins/jetpack/modules/theme-tools/content-options.php137
-rw-r--r--plugins/jetpack/modules/theme-tools/content-options/author-bio.php60
-rw-r--r--plugins/jetpack/modules/theme-tools/content-options/blog-display.php240
-rw-r--r--plugins/jetpack/modules/theme-tools/content-options/customizer.js217
-rw-r--r--plugins/jetpack/modules/theme-tools/content-options/customizer.php482
-rw-r--r--plugins/jetpack/modules/theme-tools/content-options/featured-images-fallback.php161
-rw-r--r--plugins/jetpack/modules/theme-tools/content-options/featured-images.php84
-rw-r--r--plugins/jetpack/modules/theme-tools/content-options/post-details.php150
-rw-r--r--plugins/jetpack/modules/theme-tools/featured-content.php722
-rw-r--r--plugins/jetpack/modules/theme-tools/infinite-scroll.php50
-rw-r--r--plugins/jetpack/modules/theme-tools/js/suggest.js7
-rw-r--r--plugins/jetpack/modules/theme-tools/random-redirect.php83
-rw-r--r--plugins/jetpack/modules/theme-tools/responsive-videos.php152
-rw-r--r--plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.css10
-rw-r--r--plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.js63
-rw-r--r--plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.min.js1
-rw-r--r--plugins/jetpack/modules/theme-tools/site-breadcrumbs.php79
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo.php46
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control-rtl.css12
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control.css49
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control.min.css1
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/inc/class-site-logo-control.php109
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/inc/class-site-logo.php377
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/inc/compat.php44
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/inc/functions.php176
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-control.js160
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-control.min.js1
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-header-text.js24
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-header-text.min.js1
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/js/site-logo.js46
-rw-r--r--plugins/jetpack/modules/theme-tools/site-logo/js/site-logo.min.js1
-rw-r--r--plugins/jetpack/modules/theme-tools/social-links.php252
-rw-r--r--plugins/jetpack/modules/theme-tools/social-menu.php113
-rw-r--r--plugins/jetpack/modules/theme-tools/social-menu/icon-functions.php177
-rw-r--r--plugins/jetpack/modules/theme-tools/social-menu/social-menu.css197
-rw-r--r--plugins/jetpack/modules/theme-tools/social-menu/social-menu.svg137
-rw-r--r--plugins/jetpack/modules/tiled-gallery.php36
-rw-r--r--plugins/jetpack/modules/tiled-gallery/math/class-constrained-array-rounding.php77
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery.php295
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/rtl/tiled-gallery-rtl.css96
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/carousel-container.php20
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/circle-layout.php3
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/partials/carousel-image-args.php25
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/partials/item.php65
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/rectangular-layout.php31
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/square-layout.php27
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-circle.php8
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-item.php107
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-layout.php110
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-rectangular.php223
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-rtl.css1
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-shape.php209
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-square.php70
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery.css94
-rw-r--r--plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery.js191
-rw-r--r--plugins/jetpack/modules/tonesque.php6
-rw-r--r--plugins/jetpack/modules/vaultpress.php33
-rw-r--r--plugins/jetpack/modules/verification-tools.php30
-rw-r--r--plugins/jetpack/modules/verification-tools/blog-verification-tools.php84
-rw-r--r--plugins/jetpack/modules/verification-tools/verification-tools-utils.php49
-rw-r--r--plugins/jetpack/modules/videopress.php26
-rw-r--r--plugins/jetpack/modules/videopress/class.jetpack-videopress.php340
-rw-r--r--plugins/jetpack/modules/videopress/class.videopress-ajax.php103
-rw-r--r--plugins/jetpack/modules/videopress/class.videopress-cli.php167
-rw-r--r--plugins/jetpack/modules/videopress/class.videopress-edit-attachment.php388
-rw-r--r--plugins/jetpack/modules/videopress/class.videopress-gutenberg.php169
-rw-r--r--plugins/jetpack/modules/videopress/class.videopress-options.php59
-rw-r--r--plugins/jetpack/modules/videopress/class.videopress-player.php880
-rw-r--r--plugins/jetpack/modules/videopress/class.videopress-scheduler.php197
-rw-r--r--plugins/jetpack/modules/videopress/class.videopress-video.php378
-rw-r--r--plugins/jetpack/modules/videopress/class.videopress-xmlrpc.php173
-rw-r--r--plugins/jetpack/modules/videopress/css/editor-rtl.css60
-rw-r--r--plugins/jetpack/modules/videopress/css/editor-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/videopress/css/editor.css59
-rw-r--r--plugins/jetpack/modules/videopress/css/editor.min.css2
-rw-r--r--plugins/jetpack/modules/videopress/css/videopress-editor-style-rtl.css22
-rw-r--r--plugins/jetpack/modules/videopress/css/videopress-editor-style-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/videopress/css/videopress-editor-style.css21
-rw-r--r--plugins/jetpack/modules/videopress/css/videopress-editor-style.min.css2
-rw-r--r--plugins/jetpack/modules/videopress/editor-media-view.php236
-rw-r--r--plugins/jetpack/modules/videopress/js/editor-view.js303
-rw-r--r--plugins/jetpack/modules/videopress/js/media-video-widget-extensions.js52
-rw-r--r--plugins/jetpack/modules/videopress/js/videopress-plupload.js483
-rw-r--r--plugins/jetpack/modules/videopress/js/videopress-uploader.js157
-rw-r--r--plugins/jetpack/modules/videopress/shortcode.php248
-rw-r--r--plugins/jetpack/modules/videopress/utility-functions.php692
-rw-r--r--plugins/jetpack/modules/videopress/videopress-admin-rtl.css106
-rw-r--r--plugins/jetpack/modules/videopress/videopress-admin-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/videopress/videopress-admin.css105
-rw-r--r--plugins/jetpack/modules/videopress/videopress-admin.min.css2
-rw-r--r--plugins/jetpack/modules/widget-visibility.php14
-rw-r--r--plugins/jetpack/modules/widget-visibility/widget-conditions.php852
-rw-r--r--plugins/jetpack/modules/widget-visibility/widget-conditions/rtl/widget-conditions-rtl.css115
-rw-r--r--plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions-rtl.css116
-rw-r--r--plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.css117
-rw-r--r--plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.js321
-rw-r--r--plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.min.css2
-rw-r--r--plugins/jetpack/modules/widgets.php91
-rw-r--r--plugins/jetpack/modules/widgets/authors.php271
-rw-r--r--plugins/jetpack/modules/widgets/authors/style.css25
-rw-r--r--plugins/jetpack/modules/widgets/blog-stats.php173
-rw-r--r--plugins/jetpack/modules/widgets/contact-info.php394
-rw-r--r--plugins/jetpack/modules/widgets/contact-info/contact-info-admin.js11
-rw-r--r--plugins/jetpack/modules/widgets/contact-info/contact-info-map.css4
-rw-r--r--plugins/jetpack/modules/widgets/customizer-controls.css6
-rw-r--r--plugins/jetpack/modules/widgets/customizer-utils.js119
-rw-r--r--plugins/jetpack/modules/widgets/eu-cookie-law.php301
-rw-r--r--plugins/jetpack/modules/widgets/eu-cookie-law/eu-cookie-law-admin.js32
-rw-r--r--plugins/jetpack/modules/widgets/eu-cookie-law/eu-cookie-law.js80
-rw-r--r--plugins/jetpack/modules/widgets/eu-cookie-law/form.php277
-rw-r--r--plugins/jetpack/modules/widgets/eu-cookie-law/style.css105
-rw-r--r--plugins/jetpack/modules/widgets/eu-cookie-law/widget.php25
-rw-r--r--plugins/jetpack/modules/widgets/facebook-likebox.php309
-rw-r--r--plugins/jetpack/modules/widgets/facebook-likebox/style.css3
-rw-r--r--plugins/jetpack/modules/widgets/flickr.php218
-rw-r--r--plugins/jetpack/modules/widgets/flickr/form.php93
-rw-r--r--plugins/jetpack/modules/widgets/flickr/style.css16
-rw-r--r--plugins/jetpack/modules/widgets/flickr/widget.php13
-rw-r--r--plugins/jetpack/modules/widgets/gallery.php464
-rw-r--r--plugins/jetpack/modules/widgets/gallery/css/admin-rtl.css12
-rw-r--r--plugins/jetpack/modules/widgets/gallery/css/admin-rtl.min.css1
-rw-r--r--plugins/jetpack/modules/widgets/gallery/css/admin.css11
-rw-r--r--plugins/jetpack/modules/widgets/gallery/css/admin.min.css2
-rw-r--r--plugins/jetpack/modules/widgets/gallery/css/rtl/admin-rtl.css13
-rw-r--r--plugins/jetpack/modules/widgets/gallery/js/admin.js236
-rw-r--r--plugins/jetpack/modules/widgets/gallery/js/gallery.js10
-rw-r--r--plugins/jetpack/modules/widgets/gallery/templates/form.php89
-rw-r--r--plugins/jetpack/modules/widgets/goodreads.php157
-rw-r--r--plugins/jetpack/modules/widgets/goodreads/css/goodreads.css48
-rw-r--r--plugins/jetpack/modules/widgets/goodreads/css/rtl/goodreads-rtl.css50
-rw-r--r--plugins/jetpack/modules/widgets/google-translate.php203
-rw-r--r--plugins/jetpack/modules/widgets/google-translate/google-translate.js32
-rw-r--r--plugins/jetpack/modules/widgets/gravatar-profile.css46
-rw-r--r--plugins/jetpack/modules/widgets/gravatar-profile.php435
-rw-r--r--plugins/jetpack/modules/widgets/image-widget.php274
-rw-r--r--plugins/jetpack/modules/widgets/image-widget/style.css13
-rw-r--r--plugins/jetpack/modules/widgets/internet-defense-league.php153
-rw-r--r--plugins/jetpack/modules/widgets/mailchimp.php103
-rw-r--r--plugins/jetpack/modules/widgets/migrate-to-core/gallery-widget.php198
-rw-r--r--plugins/jetpack/modules/widgets/migrate-to-core/image-widget.php220
-rw-r--r--plugins/jetpack/modules/widgets/milestone.php5
-rw-r--r--plugins/jetpack/modules/widgets/milestone/admin.js31
-rw-r--r--plugins/jetpack/modules/widgets/milestone/milestone.js49
-rw-r--r--plugins/jetpack/modules/widgets/milestone/milestone.php683
-rw-r--r--plugins/jetpack/modules/widgets/milestone/style-admin.css50
-rw-r--r--plugins/jetpack/modules/widgets/my-community.php297
-rw-r--r--plugins/jetpack/modules/widgets/my-community/style.css35
-rw-r--r--plugins/jetpack/modules/widgets/rsslinks-widget.php242
-rw-r--r--plugins/jetpack/modules/widgets/search.php815
-rw-r--r--plugins/jetpack/modules/widgets/search/css/search-widget-admin-ui.css87
-rw-r--r--plugins/jetpack/modules/widgets/search/css/search-widget-frontend.css66
-rw-r--r--plugins/jetpack/modules/widgets/search/js/search-widget-admin.js360
-rw-r--r--plugins/jetpack/modules/widgets/search/js/search-widget.js19
-rw-r--r--plugins/jetpack/modules/widgets/simple-payments.php544
-rw-r--r--plugins/jetpack/modules/widgets/simple-payments/admin-warning.php16
-rw-r--r--plugins/jetpack/modules/widgets/simple-payments/customizer.css80
-rw-r--r--plugins/jetpack/modules/widgets/simple-payments/customizer.js455
-rw-r--r--plugins/jetpack/modules/widgets/simple-payments/form.php205
-rw-r--r--plugins/jetpack/modules/widgets/simple-payments/style.css8
-rw-r--r--plugins/jetpack/modules/widgets/simple-payments/widget.php33
-rw-r--r--plugins/jetpack/modules/widgets/social-icons.php680
-rw-r--r--plugins/jetpack/modules/widgets/social-icons/social-icons-admin.css94
-rw-r--r--plugins/jetpack/modules/widgets/social-icons/social-icons-admin.js144
-rw-r--r--plugins/jetpack/modules/widgets/social-icons/social-icons.css75
-rw-r--r--plugins/jetpack/modules/widgets/social-media-icons.php346
-rw-r--r--plugins/jetpack/modules/widgets/social-media-icons/style.css49
-rw-r--r--plugins/jetpack/modules/widgets/top-posts.php674
-rw-r--r--plugins/jetpack/modules/widgets/top-posts/style.css114
-rw-r--r--plugins/jetpack/modules/widgets/twitter-timeline-admin.js35
-rw-r--r--plugins/jetpack/modules/widgets/twitter-timeline.php503
-rw-r--r--plugins/jetpack/modules/widgets/upcoming-events.php131
-rw-r--r--plugins/jetpack/modules/widgets/wordpress-post-widget.php116
-rw-r--r--plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget-base.php843
-rw-r--r--plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget.php274
-rw-r--r--plugins/jetpack/modules/widgets/wordpress-post-widget/style.css24
-rw-r--r--plugins/jetpack/modules/woocommerce-analytics/classes/wp-woocommerce-analytics-universal.php399
-rw-r--r--plugins/jetpack/modules/woocommerce-analytics/wp-woocommerce-analytics.php94
-rw-r--r--plugins/jetpack/modules/wordads.php19
-rw-r--r--plugins/jetpack/modules/wordads/css/style.css63
-rw-r--r--plugins/jetpack/modules/wordads/php/admin.php49
-rw-r--r--plugins/jetpack/modules/wordads/php/api.php143
-rw-r--r--plugins/jetpack/modules/wordads/php/cron.php48
-rw-r--r--plugins/jetpack/modules/wordads/php/networks/amazon.php3
-rw-r--r--plugins/jetpack/modules/wordads/php/params.php226
-rw-r--r--plugins/jetpack/modules/wordads/php/widgets.php115
-rw-r--r--plugins/jetpack/modules/wordads/wordads.php619
-rw-r--r--plugins/jetpack/modules/wpcc.php6
-rw-r--r--plugins/jetpack/modules/wpcom-block-editor/class-jetpack-wpcom-block-editor.php337
-rw-r--r--plugins/jetpack/modules/wpgroho.js43
703 files changed, 135592 insertions, 0 deletions
diff --git a/plugins/jetpack/modules/after-the-deadline.php b/plugins/jetpack/modules/after-the-deadline.php
new file mode 100644
index 00000000..88c13090
--- /dev/null
+++ b/plugins/jetpack/modules/after-the-deadline.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer needed.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/blocks.php b/plugins/jetpack/modules/blocks.php
new file mode 100644
index 00000000..88c13090
--- /dev/null
+++ b/plugins/jetpack/modules/blocks.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer needed.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/calypsoify/README.md b/plugins/jetpack/modules/calypsoify/README.md
new file mode 100644
index 00000000..4ce1273d
--- /dev/null
+++ b/plugins/jetpack/modules/calypsoify/README.md
@@ -0,0 +1,4 @@
+# calypsoify
+WordPress plugin for redesigning WP-Admin plugin screens to match Calypso.
+
+![](https://cldup.com/jxE-hrHGgj.png)
diff --git a/plugins/jetpack/modules/calypsoify/class.jetpack-calypsoify.php b/plugins/jetpack/modules/calypsoify/class.jetpack-calypsoify.php
new file mode 100644
index 00000000..49c44f42
--- /dev/null
+++ b/plugins/jetpack/modules/calypsoify/class.jetpack-calypsoify.php
@@ -0,0 +1,462 @@
+<?php
+/**
+ * This is Calypso skin of the wp-admin interface that is conditionally triggered via the ?calypsoify=1 param.
+ * Ported from an internal Automattic plugin.
+ */
+class Jetpack_Calypsoify {
+
+ /**
+ * Singleton instance of `Jetpack_Calypsoify`.
+ *
+ * @var object
+ */
+ public static $instance = false;
+
+ /**
+ * Is Calypsoify enabled, based on any value of `calypsoify` user meta.
+ *
+ * @var bool
+ */
+ public $is_calypsoify_enabled = false;
+
+ private function __construct() {
+ add_action( 'wp_loaded', array( $this, 'setup' ) );
+ }
+
+ public static function getInstance() {
+ if ( ! self::$instance ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ public function setup() {
+ $this->is_calypsoify_enabled = 1 == (int) get_user_meta( get_current_user_id(), 'calypsoify', true );
+ add_action( 'admin_init', array( $this, 'check_param' ), 4 );
+
+ if ( $this->is_calypsoify_enabled ) {
+ add_action( 'admin_init', array( $this, 'setup_admin' ), 6 );
+ }
+
+ // Make this always available -- in case calypsoify gets toggled off.
+ add_action( 'wp_ajax_jetpack_toggle_autoupdate', array( $this, 'jetpack_toggle_autoupdate' ) );
+ add_filter( 'handle_bulk_actions-plugins', array( $this, 'handle_bulk_actions_plugins' ), 10, 3 );
+ }
+
+ public function setup_admin() {
+ // Masterbar is currently required for this to work properly. Mock the instance of it
+ if ( ! Jetpack::is_module_active( 'masterbar' ) ) {
+ $this->mock_masterbar_activation();
+ }
+
+ if ( $this->is_page_gutenberg() ) {
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_for_gutenberg' ), 100 );
+ return;
+ }
+
+ add_action( 'admin_init', array( $this, 'check_page' ) );
+ add_action( 'admin_menu', array( $this, 'remove_core_menus' ), 100 );
+ add_action( 'admin_menu', array( $this, 'add_plugin_menus' ), 101 );
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ), 100 );
+ add_action( 'in_admin_header', array( $this, 'insert_sidebar_html' ) );
+ add_action( 'wp_before_admin_bar_render', array( $this, 'modify_masterbar' ), 100000 );
+
+ add_filter( 'get_user_option_admin_color', array( $this, 'admin_color_override' ) );
+
+ add_action( 'manage_plugins_columns', array( $this, 'manage_plugins_columns_header' ) );
+ add_action( 'manage_plugins_custom_column', array( $this, 'manage_plugins_custom_column' ), 10, 2 );
+ add_filter( 'bulk_actions-plugins', array( $this, 'bulk_actions_plugins' ) );
+
+ if ( 'plugins.php' === basename( $_SERVER['PHP_SELF'] ) ) {
+ add_action( 'admin_notices', array( $this, 'plugins_admin_notices' ) );
+ }
+ }
+
+ public function manage_plugins_columns_header( $columns ) {
+ if ( current_user_can( 'jetpack_manage_autoupdates' ) ) {
+ $columns['autoupdate'] = __( 'Automatic Update', 'jetpack' );
+ }
+ return $columns;
+ }
+
+ public function manage_plugins_custom_column( $column_name, $slug ) {
+ static $repo_plugins = array();
+
+ if ( ! current_user_can( 'jetpack_manage_autoupdates' ) ) {
+ return;
+ }
+
+ if ( empty( $repo_plugins ) ) {
+ $repo_plugins = self::get_dotorg_repo_plugins();
+ }
+
+ $autoupdating_plugins = Jetpack_Options::get_option( 'autoupdate_plugins', array() );
+ // $autoupdating_plugins_translations = Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() );
+ if ( 'autoupdate' === $column_name ) {
+ if ( ! in_array( $slug, $repo_plugins ) ) {
+ return;
+ }
+ // Shamelessly swiped from https://github.com/Automattic/wp-calypso/blob/59bdfeeb97eda4266ad39410cb0a074d2c88dbc8/client/components/forms/form-toggle
+ ?>
+
+ <span class="form-toggle__wrapper">
+ <input
+ id="autoupdate_plugin-toggle-<?php echo esc_attr( $slug ) ?>"
+ name="autoupdate_plugins[<?php echo esc_attr( $slug ) ?>]"
+ value="autoupdate"
+ class="form-toggle autoupdate-toggle"
+ type="checkbox"
+ <?php checked( in_array( $slug, $autoupdating_plugins ) ); ?>
+ readonly
+ data-slug="<?php echo esc_attr( $slug ); ?>"
+ />
+ <label class="form-toggle__label" for="autoupdate_plugin-toggle-<?php echo esc_attr( $slug ) ?>">
+ <span class="form-toggle__switch" role="checkbox"></span>
+ <span class="form-toggle__label-content"><?php /* */ ?></span>
+ </label>
+ </span>
+
+ <?php
+ }
+ }
+
+ public static function get_dotorg_repo_plugins() {
+ $plugins = get_site_transient( 'update_plugins' );
+ return array_merge( array_keys( $plugins->response ), array_keys( $plugins->no_update ) );
+ }
+
+ public function bulk_actions_plugins( $bulk_actions ) {
+ $bulk_actions['jetpack_enable_plugin_autoupdates'] = __( 'Enable Automatic Updates', 'jetpack' );
+ $bulk_actions['jetpack_disable_plugin_autoupdates'] = __( 'Disable Automatic Updates', 'jetpack' );
+ return $bulk_actions;
+ }
+
+ public function handle_bulk_actions_plugins( $redirect_to, $action, $slugs ) {
+ $redirect_to = remove_query_arg( array( 'jetpack_enable_plugin_autoupdates', 'jetpack_disable_plugin_autoupdates' ), $redirect_to );
+ if ( in_array( $action, array( 'jetpack_enable_plugin_autoupdates', 'jetpack_disable_plugin_autoupdates' ) ) ) {
+ $list = Jetpack_Options::get_option( 'autoupdate_plugins', array() );
+ $initial_qty = sizeof( $list );
+
+ if ( 'jetpack_enable_plugin_autoupdates' === $action ) {
+ $list = array_unique( array_merge( $list, $slugs ) );
+ } elseif ( 'jetpack_disable_plugin_autoupdates' === $action ) {
+ $list = array_diff( $list, $slugs );
+ }
+
+ Jetpack_Options::update_option( 'autoupdate_plugins', $list );
+ $redirect_to = add_query_arg( $action, absint( sizeof( $list ) - $initial_qty ), $redirect_to );
+ }
+ return $redirect_to;
+ }
+
+ public function plugins_admin_notices() {
+ if ( ! empty( $_GET['jetpack_enable_plugin_autoupdates'] ) ) {
+ $qty = (int) $_GET['jetpack_enable_plugin_autoupdates'];
+ printf( '<div id="message" class="updated fade"><p>' . _n( 'Enabled automatic updates on %d plugin.', 'Enabled automatic updates on %d plugins.', $qty, 'jetpack' ) . '</p></div>', $qty );
+ } elseif ( ! empty( $_GET['jetpack_disable_plugin_autoupdates'] ) ) {
+ $qty = (int) $_GET['jetpack_disable_plugin_autoupdates'];
+ printf( '<div id="message" class="updated fade"><p>' . _n( 'Disabled automatic updates on %d plugin.', 'Disabled automatic updates on %d plugins.', $qty, 'jetpack' ) . '</p></div>', $qty );
+ }
+ }
+
+ public function jetpack_toggle_autoupdate() {
+ if ( ! current_user_can( 'jetpack_manage_autoupdates' ) ) {
+ wp_send_json_error();
+ return;
+ }
+
+ $type = $_POST['type'];
+ $slug = $_POST['slug'];
+ $active = 'false' !== $_POST['active'];
+
+ check_ajax_referer( "jetpack_toggle_autoupdate-{$type}" );
+
+ if ( ! in_array( $type, array( 'plugins', 'plugins_translations' ) ) ) {
+ wp_send_json_error();
+ return;
+ }
+
+ $jetpack_option_name = "autoupdate_{$type}";
+
+ $list = Jetpack_Options::get_option( $jetpack_option_name, array() );
+
+ if ( $active ) {
+ $list = array_unique( array_merge( $list, (array) $slug ) );
+ } else {
+ $list = array_diff( $list, (array) $slug );
+ }
+
+ Jetpack_Options::update_option( $jetpack_option_name, $list );
+
+ wp_send_json_success( $list );
+ }
+
+ public function admin_color_override( $color ) {
+ return 'fresh';
+ }
+
+ public function mock_masterbar_activation() {
+ include_once JETPACK__PLUGIN_DIR . 'modules/masterbar/masterbar.php';
+ new A8C_WPCOM_Masterbar;
+ }
+
+ public function remove_core_menus() {
+ remove_menu_page( 'index.php' );
+ remove_menu_page( 'jetpack' );
+ remove_menu_page( 'edit.php' );
+ remove_menu_page( 'edit.php?post_type=feedback' );
+ remove_menu_page( 'upload.php' );
+ remove_menu_page( 'edit.php?post_type=page' );
+ remove_menu_page( 'edit-comments.php' );
+ remove_menu_page( 'themes.php' );
+ remove_menu_page( 'plugins.php' );
+ remove_menu_page( 'users.php' );
+ remove_menu_page( 'tools.php' );
+ remove_menu_page( 'link-manager.php' );
+
+ // Core settings pages
+ remove_submenu_page( 'options-general.php', 'options-general.php' );
+ remove_submenu_page( 'options-general.php', 'options-writing.php' );
+ remove_submenu_page( 'options-general.php', 'options-reading.php' );
+ remove_submenu_page( 'options-general.php', 'options-discussion.php' );
+ remove_submenu_page( 'options-general.php', 'options-media.php' );
+ remove_submenu_page( 'options-general.php', 'options-permalink.php' );
+ remove_submenu_page( 'options-general.php', 'privacy.php' );
+ remove_submenu_page( 'options-general.php', 'sharing' );
+ }
+
+ public function add_plugin_menus() {
+ global $menu, $submenu;
+
+ add_menu_page( __( 'Manage Plugins', 'jetpack' ), __( 'Manage Plugins', 'jetpack' ), 'activate_plugins', 'plugins.php', '', $this->installed_plugins_icon(), 1 );
+
+ // // Count the settings page submenus, if it's zero then don't show this.
+ if ( empty( $submenu['options-general.php'] ) ) {
+ remove_menu_page( 'options-general.php' );
+ } else {
+ // Rename and make sure the plugin settings menu is always last.
+ // Sneaky plugins seem to override this otherwise.
+ // Settings is always key 80.
+ $menu[80][0] = __( 'Plugin Settings', 'jetpack' );
+ $menu[ max( array_keys( $menu ) ) + 1 ] = $menu[80];
+ unset( $menu[80] );
+ }
+ }
+
+ public function enqueue() {
+ wp_enqueue_style( 'calypsoify_wpadminmods_css', plugin_dir_url( __FILE__ ) . 'style.min.css', false, JETPACK__VERSION );
+ wp_style_add_data( 'calypsoify_wpadminmods_css', 'rtl', 'replace' );
+ wp_style_add_data( 'calypsoify_wpadminmods_css', 'suffix', '.min' );
+
+ wp_enqueue_script( 'calypsoify_wpadminmods_js', plugin_dir_url( __FILE__ ) . 'mods.js', false, JETPACK__VERSION );
+ wp_localize_script( 'calypsoify_wpadminmods_js', 'CalypsoifyOpts', array(
+ 'nonces' => array(
+ 'autoupdate_plugins' => wp_create_nonce( 'jetpack_toggle_autoupdate-plugins' ),
+ 'autoupdate_plugins_translations' => wp_create_nonce( 'jetpack_toggle_autoupdate-plugins_translations' ),
+ )
+ ) );
+ }
+
+ public function enqueue_for_gutenberg() {
+ wp_enqueue_style( 'calypsoify_wpadminmods_css', plugin_dir_url( __FILE__ ) . 'style-gutenberg.min.css', false, JETPACK__VERSION );
+ wp_style_add_data( 'calypsoify_wpadminmods_css', 'rtl', 'replace' );
+ wp_style_add_data( 'calypsoify_wpadminmods_css', 'suffix', '.min' );
+
+ wp_enqueue_script( 'calypsoify_wpadminmods_js', plugin_dir_url( __FILE__ ) . 'mods-gutenberg.js', false, JETPACK__VERSION );
+ wp_localize_script(
+ 'calypsoify_wpadminmods_js',
+ 'calypsoifyGutenberg',
+ array(
+ 'closeUrl' => $this->get_close_gutenberg_url(),
+ )
+ );
+ }
+
+ public function insert_sidebar_html() { ?>
+ <a href="<?php echo esc_url( 'https://wordpress.com/stats/day/' . Jetpack::build_raw_urls( home_url() ) ); ?>" id="calypso-sidebar-header">
+ <svg class="gridicon gridicons-chevron-left" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M14 20l-8-8 8-8 1.414 1.414L8.828 12l6.586 6.586"></path></g></svg>
+
+ <ul>
+ <li id="calypso-sitename"><?php bloginfo( 'name' ); ?></li>
+ <li id="calypso-plugins"><?php esc_html_e( 'Plugins' ); ?></li>
+ </ul>
+ </a>
+ <?php
+ }
+
+ public function modify_masterbar() {
+ global $wp_admin_bar;
+
+ // Add proper links to masterbar top sections.
+ $my_sites_node = (object) $wp_admin_bar->get_node( 'blog' );
+ $my_sites_node->href = 'https://wordpress.com/stats/day/' . Jetpack::build_raw_urls( home_url() );
+ $wp_admin_bar->add_node( $my_sites_node );
+
+ $reader_node = (object) $wp_admin_bar->get_node( 'newdash' );
+ $reader_node->href = 'https://wordpress.com';
+ $wp_admin_bar->add_node( $reader_node );
+
+ $me_node = (object) $wp_admin_bar->get_node( 'my-account' );
+ $me_node->href = 'https://wordpress.com/me';
+ $wp_admin_bar->add_node( $me_node );
+ }
+
+ private function installed_plugins_icon() {
+ $svg = '<svg class="gridicon gridicons-plugins" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24"><g><path d="M16 8V3c0-.552-.448-1-1-1s-1 .448-1 1v5h-4V3c0-.552-.448-1-1-1s-1 .448-1 1v5H5v4c0 2.79 1.637 5.193 4 6.317V22h6v-3.683c2.363-1.124 4-3.527 4-6.317V8h-3z" fill="black"></path></g></svg>';
+
+ return 'data:image/svg+xml;base64,' . base64_encode( $svg );
+ }
+
+ /**
+ * Returns the Calypso domain that originated the current request.
+ *
+ * @return string
+ */
+ private function get_calypso_origin() {
+ $origin = ! empty( $_GET['origin'] ) ? $_GET['origin'] : 'https://wordpress.com';
+ $whitelist = array(
+ 'http://calypso.localhost:3000',
+ 'http://127.0.0.1:41050', // Desktop App
+ 'https://wpcalypso.wordpress.com',
+ 'https://horizon.wordpress.com',
+ 'https://wordpress.com',
+ );
+ return in_array( $origin, $whitelist ) ? $origin : 'https://wordpress.com';
+ }
+
+ /**
+ * Returns the site slug suffix to be used as part of the Calypso URLs. It already
+ * includes the slash separator at the beginning.
+ *
+ * @example "https://wordpress.com/block-editor" . $this->get_site_suffix()
+ *
+ * @return string
+ */
+ private function get_site_suffix() {
+ if ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'build_raw_urls' ) ) {
+ $site_suffix = Jetpack::build_raw_urls( home_url() );
+ } elseif ( class_exists( 'WPCOM_Masterbar' ) && method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
+ $site_suffix = WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
+ }
+
+ if ( $site_suffix ) {
+ return "/${site_suffix}";
+ }
+ return '';
+ }
+
+ /**
+ * Returns the Calypso URL that displays either the current post type list (if no args
+ * are supplied) or the classic editor for the current post (if a post ID is supplied).
+ *
+ * @param int|null $post_id
+ * @return string
+ */
+ public function get_calypso_url( $post_id = null ) {
+ $screen = get_current_screen();
+ $post_type = $screen->post_type;
+ if ( is_null( $post_id ) ) {
+ // E.g. `posts`, `pages`, or `types/some_custom_post_type`
+ $post_type_suffix = ( 'post' === $post_type || 'page' === $post_type )
+ ? "/${post_type}s"
+ : "/types/${post_type}";
+ $post_suffix = '';
+ } else {
+ $post_type_suffix = ( 'post' === $post_type || 'page' === $post_type )
+ ? "/${post_type}"
+ : "/edit/${post_type}";
+ $post_suffix = "/${post_id}";
+ }
+
+ return $this->get_calypso_origin() . $post_type_suffix . $this->get_site_suffix() . $post_suffix;
+ }
+
+ /**
+ * Returns the URL to be used on the block editor close button for going back to the
+ * Calypso post list.
+ *
+ * @return string
+ */
+ public function get_close_gutenberg_url() {
+ return $this->get_calypso_url();
+ }
+
+ /**
+ * Returns the URL for switching the user's editor to the Calypso (WordPress.com Classic) editor.
+ *
+ * @return string
+ */
+ public function get_switch_to_classic_editor_url() {
+ return add_query_arg(
+ 'set-editor',
+ 'classic',
+ $this->is_calypsoify_enabled ? $this->get_calypso_url( get_the_ID() ) : false
+ );
+ }
+
+ public function check_param() {
+ if ( isset( $_GET['calypsoify'] ) ) {
+ if ( 1 == (int) $_GET['calypsoify'] ) {
+ update_user_meta( get_current_user_id(), 'calypsoify', 1 );
+ } else {
+ update_user_meta( get_current_user_id(), 'calypsoify', 0 );
+ }
+
+ $page = remove_query_arg( 'calypsoify', wp_basename( $_SERVER['REQUEST_URI'] ) );
+
+ wp_safe_redirect( admin_url( $page ) );
+ }
+ }
+
+ public function check_page() {
+ // If the user hits plain /wp-admin/ then disable Calypso styles.
+ $page = wp_basename( esc_url( $_SERVER['REQUEST_URI'] ) );
+
+ if ( false !== strpos( 'index.php', $page ) || false !== strpos( 'wp-admin', $page ) ) {
+ update_user_meta( get_current_user_id(), 'calypsoify', 0 );
+ wp_safe_redirect( admin_url() );
+ die;
+ }
+ }
+
+ /**
+ * Return whether a post type should display the Gutenberg/block editor.
+ *
+ * @since 6.7.0
+ */
+ public function is_post_type_gutenberg( $post_type ) {
+ return use_block_editor_for_post_type( $post_type );
+ }
+
+ public function is_page_gutenberg() {
+ $page = wp_basename( esc_url( $_SERVER['REQUEST_URI'] ) );
+
+ if ( false !== strpos( $page, 'post-new.php' ) && empty ( $_GET['post_type'] ) ) {
+ return true;
+ }
+
+ if ( false !== strpos( $page, 'post-new.php' ) && isset( $_GET['post_type'] ) && $this->is_post_type_gutenberg( $_GET['post_type'] ) ) {
+ return true;
+ }
+
+ if ( false !== strpos( $page, 'post.php' ) ) {
+ $post = get_post( $_GET['post'] );
+ if ( isset( $post ) && isset( $post->post_type ) && $this->is_post_type_gutenberg( $post->post_type ) ) {
+ return true;
+ }
+ }
+
+ if ( false !== strpos( $page, 'revision.php' ) ) {
+ $post = get_post( $_GET['revision'] );
+ $parent = get_post( $post->post_parent );
+ if ( isset( $parent ) && isset( $parent->post_type ) && $this->is_post_type_gutenberg( $parent->post_type ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+
+$Jetpack_Calypsoify = Jetpack_Calypsoify::getInstance();
diff --git a/plugins/jetpack/modules/calypsoify/gutenberg-styles/button.scss b/plugins/jetpack/modules/calypsoify/gutenberg-styles/button.scss
new file mode 100644
index 00000000..38c256e8
--- /dev/null
+++ b/plugins/jetpack/modules/calypsoify/gutenberg-styles/button.scss
@@ -0,0 +1,143 @@
+.components-button {
+ &.is-default {
+ color: $muriel-gray-700;
+ border-color: $muriel-gray-100;
+ background: $white;
+ box-shadow: none;
+ border-width: 1px 1px 2px;
+
+ &:hover {
+ background: $white;
+ border-color: $muriel-gray-200;
+ box-shadow: none;
+ color: $muriel-gray-700;
+ }
+
+ &:focus:enabled {
+ background: $white;
+ color: $muriel-gray-700;
+ border-color: $color-primary;
+ box-shadow: 0 0 0 2px $color-primary-light;
+ }
+
+ &:active:enabled {
+ background: $white;
+ border-color: $muriel-gray-100;
+ border-width: 2px 1px 1px;
+ box-shadow: none;
+ }
+
+ &:disabled,
+ &[aria-disabled='true'] {
+ color: $muriel-gray-50;
+ background-color: $white;
+ border-color: $muriel-gray-50;
+ text-shadow: none;
+ }
+ }
+
+ &.is-primary {
+ background: $color-accent;
+ border-color: $color-accent-dark;
+ box-shadow: none;
+ color: $white;
+ text-shadow: none;
+
+ &:focus:enabled {
+ background: $muriel-hot-pink-400;
+ border-color: $color-accent;
+ color: $white;
+ box-shadow: 0 0 0 2px $color-accent-light;
+ }
+
+ &:hover {
+ box-shadow: none;
+ background: $muriel-hot-pink-400;
+ border-color: $color-accent-dark;
+ color: $white;
+ }
+
+ &:focus:enabled {
+ box-shadow: 0 0 0 2px $color-accent-light;
+ }
+
+ &:active:enabled {
+ background: $muriel-hot-pink-400;
+ border-color: $color-accent-dark;
+ box-shadow: inset 0 1px 0 $color-accent-dark;
+ }
+
+ &:disabled,
+ &[aria-disabled='true'] {
+ color: $muriel-gray-50;
+ background: $white;
+ border-color: $muriel-gray-50;
+ text-shadow: none;
+
+ &:hover,
+ &:focus,
+ &:active {
+ color: $muriel-gray-50;
+ background-color: $white;
+ border-color: $muriel-gray-50;
+ box-shadow: none;
+ }
+ }
+
+ &.is-busy,
+ &.is-busy:disabled,
+ &.is-busy[aria-disabled='true'] {
+ background-image: linear-gradient(
+ -45deg,
+ $color-accent 28%,
+ $muriel-hot-pink-600 28%,
+ $muriel-hot-pink-600 72%,
+ $color-accent 72%
+ );
+ border-color: $color-accent-dark;
+ }
+ }
+
+ /* Buttons that look like links, for a cross of good semantics with the visual */
+ &.is-link {
+ color: $color-link;
+
+ &:hover,
+ &:active {
+ color: $color-link-dark;
+ }
+
+ &:focus {
+ color: $color-link-dark;
+ box-shadow: 0 0 0 2px $color-primary-light;
+ }
+ }
+
+ /* Link buttons that are red to indicate destructive behavior. */
+ &.is-link.is-destructive {
+ color: $alert-red;
+ }
+
+ &:focus:enabled {
+ // @include button-style__focus-active;
+ }
+
+ &.is-busy {
+ background-image: repeating-linear-gradient(
+ -45deg,
+ $muriel-gray-500,
+ $white 11px,
+ $white 10px,
+ $muriel-gray-500 20px
+ );
+ }
+
+ // Buttons that are text-based.
+ &.is-tertiary {
+ color: $color-link;
+
+ &:not( :disabled ):not( [aria-disabled='true'] ):not( .is-default ):hover {
+ color: $color-link-dark;
+ }
+ }
+}
diff --git a/plugins/jetpack/modules/calypsoify/mods-gutenberg.js b/plugins/jetpack/modules/calypsoify/mods-gutenberg.js
new file mode 100644
index 00000000..06b7b015
--- /dev/null
+++ b/plugins/jetpack/modules/calypsoify/mods-gutenberg.js
@@ -0,0 +1,28 @@
+/* eslint-disable no-var */
+/* global wp, calypsoifyGutenberg */
+
+jQuery( function( $ ) {
+ if (
+ wp &&
+ wp.data &&
+ wp.data.select &&
+ ! wp.data.select( 'core/edit-post' ).isFeatureActive( 'fullscreenMode' )
+ ) {
+ wp.data.dispatch( 'core/edit-post' ).toggleFeature( 'fullscreenMode' );
+ }
+
+ var editPostHeaderInception = setInterval( function() {
+ var $closeButton = $( '.edit-post-fullscreen-mode-close__toolbar a' );
+ if ( $closeButton.length < 1 ) {
+ return;
+ }
+ clearInterval( editPostHeaderInception );
+
+ $closeButton.attr( 'href', calypsoifyGutenberg.closeUrl );
+ } );
+
+ $( 'body.revision-php a' ).each( function() {
+ var href = $( this ).attr( 'href' );
+ $( this ).attr( 'href', href.replace( '&classic-editor', '' ) );
+ } );
+} );
diff --git a/plugins/jetpack/modules/calypsoify/mods.js b/plugins/jetpack/modules/calypsoify/mods.js
new file mode 100644
index 00000000..4af41a72
--- /dev/null
+++ b/plugins/jetpack/modules/calypsoify/mods.js
@@ -0,0 +1,89 @@
+/* global pagenow, ajaxurl, CalypsoifyOpts */
+( function( $ ) {
+ $( window ).load( function() {
+ // On Plugins.php
+ if ( 'plugins' === pagenow ) {
+ // pagenow === $current_screen->id
+ // Remove | and () from the plugins filter bar
+ $.each( $( 'ul.subsubsub li' ), function( i, el ) {
+ var li = $( el );
+ li.html(
+ li
+ .html()
+ .replace( '|', '' )
+ .replace( '(', '' )
+ .replace( ')', '' )
+ );
+ } );
+
+ // Add in the AJAX-y goodness for toggling autoupdates.
+ $( 'input.autoupdate-toggle' ).change( function( event ) {
+ var el = event.target;
+
+ el.disabled = true;
+ el.classList.add( 'is-toggling' );
+
+ jQuery.post(
+ ajaxurl,
+ {
+ action: 'jetpack_toggle_autoupdate',
+ type: 'plugins',
+ slug: el.dataset.slug,
+ active: el.checked,
+ _wpnonce: CalypsoifyOpts.nonces.autoupdate_plugins,
+ },
+ function() {
+ // Add something to test and confirm that `el.dataset.slug` is missing from `response.data` ?
+ el.disabled = false;
+ el.classList.remove( 'is-toggling' );
+ }
+ );
+ } );
+ }
+
+ $( '#wp-admin-bar-root-default' ).on( 'click', 'li', function( event ) {
+ location.href = $( event.target )
+ .closest( 'a' )
+ .attr( 'href' );
+ } );
+
+ $( '#wp-admin-bar-top-secondary' ).on( 'click', 'li#wp-admin-bar-my-account', function(
+ event
+ ) {
+ location.href = $( event.target )
+ .closest( 'a' )
+ .attr( 'href' );
+ } );
+
+ if ( document && document.location && document.location.search ) {
+ var params_array = document.location.search.substr( 1 ).split( '&' ),
+ params_object = {},
+ body = $( document.body ),
+ i,
+ key_value,
+ pluginEl;
+
+ if ( params_array && params_array.length ) {
+ for ( i = 0; i < params_array.length; i++ ) {
+ key_value = params_array[ i ].split( '=' );
+ params_object[ key_value[ 0 ] ] = key_value[ 1 ];
+ }
+
+ if ( params_object.s && params_object[ 'modal-mode' ] && params_object.plugin ) {
+ pluginEl = $(
+ '.plugin-card-' + params_object.plugin + ' .thickbox.open-plugin-details-modal'
+ );
+ if ( pluginEl && pluginEl.length ) {
+ pluginEl.click();
+ }
+ }
+ }
+
+ body.on( 'thickbox:iframe:loaded', function() {
+ $( '#TB_window' ).on( 'click', 'button#TB_closeWindowButton', function() {
+ $( '#TB_closeWindowButton' ).click();
+ } );
+ } );
+ }
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/calypsoify/style-gutenberg-rtl.min.css b/plugins/jetpack/modules/calypsoify/style-gutenberg-rtl.min.css
new file mode 100644
index 00000000..002de0ee
--- /dev/null
+++ b/plugins/jetpack/modules/calypsoify/style-gutenberg-rtl.min.css
@@ -0,0 +1,4 @@
+/* Do not modify this file directly. It is compiled SASS code. */
+.components-button.is-default{color:#3d4145;border-color:#ccced0;background:#fff;box-shadow:none;border-width:1px 1px 2px}.components-button.is-default:hover{background:#fff;border-color:#b0b5b8;box-shadow:none;color:#3d4145}.components-button.is-default:focus:enabled{background:#fff;color:#3d4145;border-color:#016087;box-shadow:0 0 0 2px #6f93ad}.components-button.is-default:active:enabled{background:#fff;border-color:#ccced0;border-width:2px 1px 1px;box-shadow:none}.components-button.is-default:disabled,.components-button.is-default[aria-disabled='true']{color:#e1e2e2;background-color:#fff;border-color:#e1e2e2;text-shadow:none}.components-button.is-primary{background:#d52c82;border-color:#992053;box-shadow:none;color:#fff;text-shadow:none}.components-button.is-primary:focus:enabled{background:#ff3997;border-color:#d52c82;color:#fff;box-shadow:0 0 0 2px #ff76b8}.components-button.is-primary:hover{box-shadow:none;background:#ff3997;border-color:#992053;color:#fff}.components-button.is-primary:focus:enabled{box-shadow:0 0 0 2px #ff76b8}.components-button.is-primary:active:enabled{background:#ff3997;border-color:#992053;box-shadow:inset 0 1px 0 #992053}.components-button.is-primary:disabled,.components-button.is-primary[aria-disabled='true']{color:#e1e2e2;background:#fff;border-color:#e1e2e2;text-shadow:none}.components-button.is-primary:disabled:hover,.components-button.is-primary:disabled:focus,.components-button.is-primary:disabled:active,.components-button.is-primary[aria-disabled='true']:hover,.components-button.is-primary[aria-disabled='true']:focus,.components-button.is-primary[aria-disabled='true']:active{color:#e1e2e2;background-color:#fff;border-color:#e1e2e2;box-shadow:none}.components-button.is-primary.is-busy,.components-button.is-primary.is-busy:disabled,.components-button.is-primary.is-busy[aria-disabled='true']{background-image:linear-gradient(45deg, #d52c82 28%, #b7266a 28%, #b7266a 72%, #d52c82 72%);border-color:#992053}.components-button.is-link{color:#016087}.components-button.is-link:hover,.components-button.is-link:active{color:#23354b}.components-button.is-link:focus{color:#23354b;box-shadow:0 0 0 2px #6f93ad}.components-button.is-link.is-destructive{color:#ff4b1c}.components-button.is-busy{background-image:repeating-linear-gradient(45deg, #636d75, #fff 11px, #fff 10px, #636d75 20px)}.components-button.is-tertiary{color:#016087}.components-button.is-tertiary:not(:disabled):not([aria-disabled='true']):not(.is-default):hover{color:#23354b}.edit-post-more-menu__content .components-menu-group:first-child .components-menu-item__button:last-child{display:none}.editor-inserter__manage-reusable-blocks,a.components-menu-item__button[href*="post_type=wp_block"]{display:none}.edit-post-sidebar__panel-tab.is-active{border-color:#016087}.edit-post-sidebar .input-control:focus,.edit-post-sidebar input[type='checkbox']:focus,.edit-post-sidebar input[type='color']:focus,.edit-post-sidebar input[type='date']:focus,.edit-post-sidebar input[type='datetime-local']:focus,.edit-post-sidebar input[type='datetime']:focus,.edit-post-sidebar input[type='email']:focus,.edit-post-sidebar input[type='month']:focus,.edit-post-sidebar input[type='number']:focus,.edit-post-sidebar input[type='password']:focus,.edit-post-sidebar input[type='radio']:focus,.edit-post-sidebar input[type='search']:focus,.edit-post-sidebar input[type='tel']:focus,.edit-post-sidebar input[type='text']:focus,.edit-post-sidebar input[type='time']:focus,.edit-post-sidebar input[type='url']:focus,.edit-post-sidebar input[type='week']:focus,.edit-post-sidebar select:focus,.edit-post-sidebar textarea:focus{border-color:#016087;box-shadow:0 0 0 2px #6f93ad}.edit-post-sidebar input[type='checkbox']:checked{background:#016087;border-color:#016087}a{color:#016087}a:active,a:hover{color:#23354b}a:focus{color:#23354b;box-shadow:none;outline:thin dotted}.wp-toolbar .revision-php{margin-top:-32px}.revision-php{background:#f6f6f6}.revision-php #wpadminbar,.revision-php #adminmenumain,.revision-php #wp-admin-bar-menu-toggle{display:none}.revision-php #wpcontent{margin-right:0 !important}.revision-php #wpbody{padding-top:0}.revision-php #screen-meta-links{display:none !important}.revision-php #wpfooter{display:none !important}.revision-tickmarks{margin-top:8px}.revisions-controls{height:118px}.revisions-controls .author-card .avatar{border-radius:50%;height:38px;margin-top:4px;width:38px}.revisions-controls .author-card .author-info{line-height:20px;margin-top:4px}.comparing-two-revisions .revisions-controls{height:176px}.revisions-meta{margin-top:28px}.diff-meta{min-height:46px}.revision-toggle-compare-mode label{vertical-align:top}.revisions-tooltip{transform:translateY(-36px)}
+
+/*# sourceMappingURL=style-gutenberg-rtl.min.css.map */
diff --git a/plugins/jetpack/modules/calypsoify/style-gutenberg.min.css b/plugins/jetpack/modules/calypsoify/style-gutenberg.min.css
new file mode 100644
index 00000000..cb47e755
--- /dev/null
+++ b/plugins/jetpack/modules/calypsoify/style-gutenberg.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is compiled SASS code. */
+.components-button.is-default{color:#3d4145;border-color:#ccced0;background:#fff;box-shadow:none;border-width:1px 1px 2px}.components-button.is-default:hover{background:#fff;border-color:#b0b5b8;box-shadow:none;color:#3d4145}.components-button.is-default:focus:enabled{background:#fff;color:#3d4145;border-color:#016087;box-shadow:0 0 0 2px #6f93ad}.components-button.is-default:active:enabled{background:#fff;border-color:#ccced0;border-width:2px 1px 1px;box-shadow:none}.components-button.is-default:disabled,.components-button.is-default[aria-disabled='true']{color:#e1e2e2;background-color:#fff;border-color:#e1e2e2;text-shadow:none}.components-button.is-primary{background:#d52c82;border-color:#992053;box-shadow:none;color:#fff;text-shadow:none}.components-button.is-primary:focus:enabled{background:#ff3997;border-color:#d52c82;color:#fff;box-shadow:0 0 0 2px #ff76b8}.components-button.is-primary:hover{box-shadow:none;background:#ff3997;border-color:#992053;color:#fff}.components-button.is-primary:focus:enabled{box-shadow:0 0 0 2px #ff76b8}.components-button.is-primary:active:enabled{background:#ff3997;border-color:#992053;box-shadow:inset 0 1px 0 #992053}.components-button.is-primary:disabled,.components-button.is-primary[aria-disabled='true']{color:#e1e2e2;background:#fff;border-color:#e1e2e2;text-shadow:none}.components-button.is-primary:disabled:hover,.components-button.is-primary:disabled:focus,.components-button.is-primary:disabled:active,.components-button.is-primary[aria-disabled='true']:hover,.components-button.is-primary[aria-disabled='true']:focus,.components-button.is-primary[aria-disabled='true']:active{color:#e1e2e2;background-color:#fff;border-color:#e1e2e2;box-shadow:none}.components-button.is-primary.is-busy,.components-button.is-primary.is-busy:disabled,.components-button.is-primary.is-busy[aria-disabled='true']{background-image:linear-gradient(-45deg, #d52c82 28%, #b7266a 28%, #b7266a 72%, #d52c82 72%);border-color:#992053}.components-button.is-link{color:#016087}.components-button.is-link:hover,.components-button.is-link:active{color:#23354b}.components-button.is-link:focus{color:#23354b;box-shadow:0 0 0 2px #6f93ad}.components-button.is-link.is-destructive{color:#ff4b1c}.components-button.is-busy{background-image:repeating-linear-gradient(-45deg, #636d75, #fff 11px, #fff 10px, #636d75 20px)}.components-button.is-tertiary{color:#016087}.components-button.is-tertiary:not(:disabled):not([aria-disabled='true']):not(.is-default):hover{color:#23354b}.edit-post-more-menu__content .components-menu-group:first-child .components-menu-item__button:last-child{display:none}.editor-inserter__manage-reusable-blocks,a.components-menu-item__button[href*="post_type=wp_block"]{display:none}.edit-post-sidebar__panel-tab.is-active{border-color:#016087}.edit-post-sidebar .input-control:focus,.edit-post-sidebar input[type='checkbox']:focus,.edit-post-sidebar input[type='color']:focus,.edit-post-sidebar input[type='date']:focus,.edit-post-sidebar input[type='datetime-local']:focus,.edit-post-sidebar input[type='datetime']:focus,.edit-post-sidebar input[type='email']:focus,.edit-post-sidebar input[type='month']:focus,.edit-post-sidebar input[type='number']:focus,.edit-post-sidebar input[type='password']:focus,.edit-post-sidebar input[type='radio']:focus,.edit-post-sidebar input[type='search']:focus,.edit-post-sidebar input[type='tel']:focus,.edit-post-sidebar input[type='text']:focus,.edit-post-sidebar input[type='time']:focus,.edit-post-sidebar input[type='url']:focus,.edit-post-sidebar input[type='week']:focus,.edit-post-sidebar select:focus,.edit-post-sidebar textarea:focus{border-color:#016087;box-shadow:0 0 0 2px #6f93ad}.edit-post-sidebar input[type='checkbox']:checked{background:#016087;border-color:#016087}a{color:#016087}a:active,a:hover{color:#23354b}a:focus{color:#23354b;box-shadow:none;outline:thin dotted}.wp-toolbar .revision-php{margin-top:-32px}.revision-php{background:#f6f6f6}.revision-php #wpadminbar,.revision-php #adminmenumain,.revision-php #wp-admin-bar-menu-toggle{display:none}.revision-php #wpcontent{margin-left:0 !important}.revision-php #wpbody{padding-top:0}.revision-php #screen-meta-links{display:none !important}.revision-php #wpfooter{display:none !important}.revision-tickmarks{margin-top:8px}.revisions-controls{height:118px}.revisions-controls .author-card .avatar{border-radius:50%;height:38px;margin-top:4px;width:38px}.revisions-controls .author-card .author-info{line-height:20px;margin-top:4px}.comparing-two-revisions .revisions-controls{height:176px}.revisions-meta{margin-top:28px}.diff-meta{min-height:46px}.revision-toggle-compare-mode label{vertical-align:top}.revisions-tooltip{transform:translateY(-36px)}
diff --git a/plugins/jetpack/modules/calypsoify/style-rtl.min.css b/plugins/jetpack/modules/calypsoify/style-rtl.min.css
new file mode 100644
index 00000000..1fdb4250
--- /dev/null
+++ b/plugins/jetpack/modules/calypsoify/style-rtl.min.css
@@ -0,0 +1,4 @@
+/* Do not modify this file directly. It is compiled SASS code. */
+body,#wp-content-editor-tools{background:#f6f6f6}#wpwrap{top:14px}#wp-admin-bar-notes #wpnt-notes-unread-count.wpn-unread{background-image:none !important;background-color:#ff76b8 !important;border:none !important}#adminmenu #collapse-menu,#adminmenu .wp-menu-separator,#screen-meta-links,.wp-submenu,#toplevel_page_jetpack{display:none}.wp-menu-open .wp-submenu{display:block}#adminmenuwrap,#adminmenuback,#adminmenu{background:#fff}#adminmenuback{border-left:1px solid #e1e2e2}#adminmenu,#adminmenuwrap,#adminmenuback,#adminmenu .wp-submenu{width:272px}#adminmenu{margin-top:71px}#adminmenu .wp-has-current-submenu .wp-submenu,#adminmenu .opensub .wp-submenu,#adminmenu .opensub .wp-submenu:after,#adminmenu a.wp-has-current-submenu:focus+.wp-submenu{background:transparent !important}#adminmenu li.menu-top:hover,#adminmenu li.opensub>a.menu-top,#adminmenu li>a.menu-top:focus,#adminmenu li.wp-menu-open,#adminmenu a:hover{background:#f6f6f6}#adminmenu .wp-submenu-head,#adminmenu a.menu-top{padding:5px 5px 5px 0}#adminmenu .wp-has-current-submenu ul>li>a{padding:11px 20px 11px 16px;font-size:14px}#adminmenu .wp-submenu a:hover{background-color:#e1e2e2}#adminmenu>li.wp-first-item{border-bottom:1px solid rgba(200,215,225,0.5)}#adminmenu a.wp-has-current-submenu:after,#adminmenu>li.current>a.current:after,#adminmenu li.wp-has-submenu.wp-not-current-submenu.opensub:hover:after{border:none}#adminmenu .dashicons,#adminmenu .dashicons-before:before{width:24px;height:24px;font-size:24px}#adminmenu a,#adminmenu div.wp-menu-image:before{color:#3d4145 !important}#adminmenu li.current a.menu-top,#adminmenu li.wp-has-current-submenu a.wp-has-current-submenu{background:#d8dee4}#adminmenu div.wp-menu-image.svg{filter:brightness(0.25)}#adminmenu li.wp-menu-open div.wp-menu-image.svg{filter:brightness(100)}#adminmenu li.wp-menu-open div.wp-menu-image:before,#adminmenu li.wp-menu-open div.wp-menu-name{color:#016087 !important}#adminmenu div.wp-menu-name{color:#2b2d2f;font-size:15px;padding:9px 41px 8px 0}#adminmenu li.menu-top{min-height:46px}#adminmenu .awaiting-mod,#adminmenu .update-plugins{background-color:#016087}.no-js li.wp-has-current-submenu:hover .wp-submenu{background:transparent !important}#wpcontent,#wpfooter{margin-right:272px}#toplevel_page_plugins div.wp-menu-image.svg,#toplevel_page_plugin-install div.wp-menu-image.svg{background-size:24px auto}#toplevel_page_plugins div.wp-menu-image.svg{position:relative;right:-2px}#calypso-sidebar-header{position:fixed;top:47px;right:0;width:272px;height:70px;background:#fff;z-index:10000}#calypso-sidebar-header svg{float:right;position:relative;right:10px;top:23px}#calypso-sidebar-header ul{float:right;position:relative;top:3px;right:15px}#calypso-sidebar-header ul li{margin:0}#calypso-sidebar-header ul li#calypso-sitename{font-size:12px;color:#636d75;overflow:hidden;white-space:nowrap;width:225px}@media screen and (max-width: 782px){#calypso-sidebar-header ul li#calypso-sitename{width:150px}}#calypso-sidebar-header ul li#calypso-sitename:after{content:'';display:block;position:absolute;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;background:linear-gradient(to left, rgba(255,255,255,0), #fff 90%);top:0px;bottom:0px;left:0px;right:auto;width:20%;height:auto}#calypso-sidebar-header ul li#calypso-plugins{font-weight:bold;color:#2b2d2f;font-size:16px}.folded #adminmenu .wp-has-current-submenu .wp-submenu,.folded #adminmenu .opensub .wp-submenu,.folded #adminmenu .opensub .wp-submenu:after,.folded #adminmenu a.wp-has-current-submenu:focus+.wp-submenu{background:#f6f6f6 !important}.folded #adminmenu li.menu-top .wp-submenu>li>a{padding:7px 46px 7px 12px;font-size:14px}.folded #adminmenu li.current.menu-top,.folded #adminmenu li.wp-has-current-submenu,.folded #adminmenu li.wp-has-current-submenu .wp-submenu .wp-submenu-head{background:#f3f5f6}.folded #adminmenu .wp-submenu .wp-submenu-head{padding:14px 11px 14px 4px}.folded #adminmenu a.menu-top{padding-right:1px}.folded #wpcontent #calypso-sidebar-header{width:36px}.folded #wpcontent #calypso-sidebar-header svg{right:6px}.folded #wpcontent #calypso-sidebar-header ul{display:none}.folded .no-js li.wp-has-current-submenu:hover .wp-submenu{background:#f6f6f6 !important}.folded #toplevel_page_plugins div.wp-menu-image.svg{position:relative;right:-2px}@media only screen and (max-width: 960px){#calypso-sidebar-header{width:36px}#calypso-sidebar-header ul{display:none}#calypso-sidebar-header svg{right:6px}#adminmenu a.menu-top{padding-right:1px}}@media screen and (max-width: 782px){#calypso-sidebar-header{position:absolute;display:none;width:190px;top:-14px}.wp-responsive-open #calypso-sidebar-header{display:block}#calypso-sidebar-header ul{display:block}.auto-fold #adminmenu .wp-menu-name{margin-right:0}.auto-fold #adminmenu{top:-14px}.auto-fold #adminmenu .selected,#adminmenu li.opensub>a.menu-top,#adminmenu li>a.menu-top:focus,#adminmenu li.wp-menu-open{background:#d8dee4 !important}#adminmenu .wp-submenu,.auto-fold #adminmenu .selected .wp-submenu,.auto-fold #adminmenu .wp-menu-open .wp-submenu,#adminmenu a.wp-has-current-submenu:focus+.wp-submenu{background:#fff !important}.auto-fold #adminmenu li.selected div.wp-menu-image.svg{filter:brightness(100)}.auto-fold #adminmenu li.selected div.wp-menu-image:before,.auto-fold #adminmenu li.selected div.wp-menu-name{color:#016087 !important}#wpadminbar .quicklinks>ul>li>a,#wpadminbar .quicklinks>ul>li>.ab-empty-item{padding:0 15px !important}#wpadminbar li#wp-admin-bar-ab-new-post a{padding:7px 15px !important}}@media screen and (max-width: 600px){#calypso-sidebar-header{top:32px}.auto-fold #adminmenu{top:32px}}@media screen and (max-width: 480px){#wpadminbar #wp-admin-bar-blog.my-sites>a.ab-item:before{margin-top:4px !important}#wpadminbar #wp-admin-bar-newdash>a.ab-item:before{margin-top:6px !important}#wpadminbar ul li#wp-admin-bar-ab-new-post a:before{top:-5px !important;margin-right:-12px !important}}.nav-tab-wrapper,.wrap h2.nav-tab-wrapper{margin:10px 0 25px;background:#fff;border:1px solid rgba(200,215,225,0.5)}.nav-tab{border:none;background:none;font-weight:400;padding:3px 13px 12px;color:#016087}.nav-tab-active,.nav-tab-active:focus,.nav-tab-active:focus:active,.nav-tab-active:hover{background:transparent;box-shadow:none}.nav-tab:first-child{margin-right:0}.nav-tab-active,.nav-tab-active:focus,.nav-tab-active:focus:active{border-bottom:2px solid #3d4145;color:#2b2d2f}#wpadminbar{background:#016087;-webkit-box-shadow:none;-mozilla-box-shadow:none;height:46px;position:fixed}#wpadminbar .ab-top-menu>li>.ab-item{font-size:14px}#wpadminbar .ab-top-menu>li.hover>.ab-item{background:#6f93ad !important;color:#fff}#wpadminbar *{line-height:46px}#wpadminbar .quicklinks a,#wpadminbar .quicklinks .ab-empty-item,#wpadminbar .shortlink-input{height:46px}#wpadminbar .quicklinks>ul>li>a{padding:0 15px}#wpadminbar .quicklinks>ul>li.current>a{background:#004966}#wpadminbar:not(.mobile) .ab-top-menu>li>.ab-item:focus,#wpadminbar.nojq .quicklinks .ab-top-menu>li>.ab-item:focus,#wpadminbar .ab-top-menu>li.ab-hover>.ab-item{background:transparent !important}#wpadminbar:not(.mobile) .ab-top-menu>li:hover>.ab-item{background:#6f93ad !important;color:#fff}#wpadminbar .ab-top-menu>li.my-sites>.ab-item,#wpadminbar .ab-top-menu>li.my-sites.hover>.ab-item,#wpadminbar .ab-top-menu>li.my-sites.ab-hover>.ab-item{background:#004966 !important}#wpadminbar #wp-admin-bar-blog.my-sites>a.ab-item:before,#wpadminbar #wp-admin-bar-newdash>a.ab-item:before{margin-top:12px}#wpadminbar ul li#wp-admin-bar-ab-new-post{border-radius:3px}#wpadminbar ul li#wp-admin-bar-ab-new-post a{padding:6px 15px;color:#016087 !important}#wpadminbar ul li#wp-admin-bar-ab-new-post a span{color:#016087 !important;font-size:14px !important}#wpadminbar ul li#wp-admin-bar-ab-new-post a:before,#wpadminbar ul li#wp-admin-bar-ab-new-post a:after{background-image:url('data:image/svg+xml;utf8,<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect x="0" fill="none" width="24" height="24"/><g><path fill="%230087be" d="M21 14v5c0 1.105-.895 2-2 2H5c-1.105 0-2-.895-2-2V5c0-1.105.895-2 2-2h5v2H5v14h14v-5h2z"/><path fill="%230087be" d="M21 7h-4V3h-2v4h-4v2h4v4h2V9h4"/></g></svg>') !important}#wpadminbar ul li#wp-admin-bar-ab-new-post:hover,#wpadminbar ul li#wp-admin-bar-ab-new-post:hover>.ab-item{background:#f6f6f6 !important;opacity:1;border-radius:3px !important}#wpadminbar li#wp-admin-bar-blog.menupop>.ab-sub-wrapper,#wpadminbar li#wp-admin-bar-newdash.menupop>.ab-sub-wrapper,#wpadminbar li#wp-admin-bar-my-account.menupop>.ab-sub-wrapper{display:none !important}#wpadminbar li#wp-admin-bar-notes.active,#wpadminbar li#wp-admin-bar-notes.active>.ab-item{background:#004966 !important}#wpadminbar li#wp-admin-bar-notes>#wpnt-notes-panel2{top:46px}#wpadminbar .ab-top-menu>li.ab-active>.ab-item,#wpadminbar>#wp-toolbar .wpnt-show span.noticon,#wpadminbar #wp-admin-bar-notes.wpnt-show .noticon{color:#fff !important}#wpadminbar .ab-active>a.ab-item:before,#wpadminbar #wp-admin-bar-notes.active .noticon-bell:before{filter:brightness(100) !important}#wpadminbar .quicklinks>ul>li#wp-admin-bar-notes>a.ab-item span.noticon,#wpadminbar>#wp-toolbar span.noticon,#wpadminbar #wp-admin-bar-notes .noticon{top:10px}#wpadminbar>#wp-toolbar>#wp-admin-bar-root-default .ab-icon,#wpadminbar .ab-icon,#wpadminbar .ab-item:before{font-size:24px;line-height:1.45}.wrap{margin:20px 15px 25px 30px}@media screen and (max-width: 782px){.wrap{margin:10px 7px 10px 18px}}.subsubsub,.wp-filter{margin:10px 0 25px;background:#fff;border:1px solid rgba(200,215,225,0.5);width:100%;box-shadow:none;padding:0}.subsubsub a,.filter-links li>a{padding:10px 15px;display:inline-block;font-size:14px;margin:0;color:#016087;border-bottom:2px solid #fff;outline:none}.subsubsub a:focus,.filter-links li>a:focus{box-shadow:0 0 0 1px #016087,0 0 2px 1px #6f93ad}.subsubsub a:hover,.filter-links li>a:hover{color:#23354b;background-color:#f3f5f6}.subsubsub a:hover:not(.current),.filter-links li>a:hover:not(.current){border-color:#f3f5f6}.filter-links li>a{padding:16px}.subsubsub a.current,.filter-links .current{border-bottom:2px solid #3d4145}.count{display:inline-block;padding:1px 6px;border:solid 1px #969ca1;border-radius:12px;font-size:11px;font-weight:bold;line-height:14px;color:#636d75;text-align:center;margin-right:2px}.plugins-php .plugins a{color:#016087}.plugins-php .plugins a:hover,.plugins-php .plugins a:focus{color:#23354b}.plugins-php .plugins a:focus{box-shadow:0 0 0 1px #016087,0 0 2px 1px #6f93ad}.plugins-php .plugins a.delete{color:#eb0001}.plugins-php .plugins a.delete:hover,.plugins-php .plugins a.delete:focus{color:#ac120b}.plugins-php .plugins a.delete:focus{box-shadow:0 0 0 1px #eb0001,0 0 2px 1px #ff8248}.plugins-php .tablenav{clear:none;float:right;margin-bottom:15px}.plugins-php .tablenav .one-page .displaying-num{display:none}.plugins-php .bulkactions select:focus{border-color:#016087;box-shadow:0 0 2px #6f93ad}.plugins-php p.search-box{margin-top:5px}.plugins-php p.search-box .wp-filter-search:focus{border-color:#016087;box-shadow:0 0 2px #6f93ad}.plugins-php .plugin-update-tr.active td,.plugins-php .plugins .active th{border-right:4px solid #016087}.plugins-php .plugins .active th,.plugins-php .plugins .active td,.plugins-php .plugins .active th.check-column,.plugins-php .plugin-update-tr.active td{background-color:#f3f5f6}.wrap .wp-heading-inline+.page-title-action,.wrap .add-new-h2,.wrap .add-new-h2:active,.wrap .page-title-action,.wrap .page-title-action:active{background:#d52c82;border-color:#992053;color:#fff;border-style:solid;border-width:1px 1px 2px;cursor:pointer;display:inline-block;margin:0 0 0 5px;outline:0;overflow:hidden;font-weight:500;text-overflow:ellipsis;text-decoration:none;vertical-align:middle;box-sizing:border-box;font-size:13px;line-height:21px;border-radius:4px;padding:2px 10px 2px;margin-bottom:2px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.wrap .wp-heading-inline+.page-title-action:hover,.wrap .add-new-h2:hover,.wrap .add-new-h2:active:hover,.wrap .page-title-action:hover,.wrap .page-title-action:active:hover{background-color:#ff3997}.wrap .wp-heading-inline+.page-title-action:focus,.wrap .add-new-h2:focus,.wrap .add-new-h2:active:focus,.wrap .page-title-action:focus,.wrap .page-title-action:active:focus{box-shadow:0 0 0 2px #ff76b8;background-color:#ff3997}.wp-core-ui .button{background:#fff}.wp-core-ui .button:focus{border-color:#016087;box-shadow:0 0 3px #6f93ad}.wp-core-ui .button-primary{background:#016087;border-color:#23354b;color:#fff;text-shadow:none}.wp-core-ui .button-primary:hover,.wp-core-ui .button-primary:focus{background-color:#46799a}.ui-tabs-nav li,.wp-switch-editor{background-color:#f6f6f6 !important}.plugin-card a,.popular-tags a,.filter-links>li>a{color:#016087}.plugin-card a:hover,.plugin-card a:focus,.popular-tags a:hover,.popular-tags a:focus,.filter-links>li>a:hover,.filter-links>li>a:focus{color:#23354b}.plugin-card a:focus,.popular-tags a:focus,.filter-links>li>a:focus{box-shadow:0 0 0 1px #016087,0 0 2px 1px #6f93ad}.plugin-card-bottom,.alternate,.striped>tbody>:nth-child(odd),ul.striped>:nth-child(odd),.ui-tabs-panel,.ui-tabs-nav li.ui-tabs-active,.ui-tabs-nav li.ui-tabs-active:hover,div.mce-toolbar-grp,.html-active .switch-html,.tmce-active .switch-tmce,#post-status-info,.quicktags-toolbar,#major-publishing-actions{background-color:#fff;border-color:#d7e1e9}.wp-filter .search-form{margin-left:10px}.form-toggle[type="checkbox"]{display:none}.form-toggle__switch{position:relative;display:inline-block;border-radius:12px;box-sizing:border-box;padding:2px;width:40px;height:24px;vertical-align:middle;align-self:flex-start;outline:0;cursor:pointer;transition:all .4s ease, box-shadow 0s}.form-toggle__switch:before,.form-toggle__switch:after{position:relative;display:block;content:"";width:20px;height:20px}.form-toggle__switch:after{right:0;border-radius:50%;background:#fff;transition:all .2s ease}.form-toggle__switch:before{display:none}.accessible-focus .form-toggle__switch:focus{box-shadow:0 0 0 2px #016087}.form-toggle__label{cursor:pointer}.is-disabled .form-toggle__label{cursor:default}.form-toggle__label .form-toggle__label-content{flex:0 1 100%;margin-right:12px}.accessible-focus .form-toggle:focus+.form-toggle__label .form-toggle__switch{box-shadow:0 0 0 2px #016087}.accessible-focus .form-toggle:focus:checked+.form-toggle__label .form-toggle__switch{box-shadow:0 0 0 2px #6f93ad}.form-toggle+.form-toggle__label .form-toggle__switch{background:#b0b5b8}.form-toggle:not(:disabled)+.form-toggle__label:hover .form-toggle__switch{background:#ccced0}.form-toggle:checked+.form-toggle__label .form-toggle__switch{background:#016087}.form-toggle:checked+.form-toggle__label .form-toggle__switch:after{right:16px}.form-toggle:checked:not(:disabled)+.form-toggle__label:hover .form-toggle__switch{background:#6f93ad}.form-toggle:disabled+label.form-toggle__label span.form-toggle__switch{opacity:0.25;cursor:default}.form-toggle.is-toggling+.form-toggle__label .form-toggle__switch{background:#016087}.form-toggle.is-toggling:checked+.form-toggle__label .form-toggle__switch{background:#ccced0}.form-toggle.is-compact+.form-toggle__label .form-toggle__switch{border-radius:8px;width:24px;height:16px}.form-toggle.is-compact+.form-toggle__label .form-toggle__switch:before,.form-toggle.is-compact+.form-toggle__label .form-toggle__switch:after{width:12px;height:12px}.form-toggle.is-compact:checked+.form-toggle__label .form-toggle__switch:after{right:8px}
+
+/*# sourceMappingURL=style-rtl.min.css.map */
diff --git a/plugins/jetpack/modules/calypsoify/style.min.css b/plugins/jetpack/modules/calypsoify/style.min.css
new file mode 100644
index 00000000..a21c4beb
--- /dev/null
+++ b/plugins/jetpack/modules/calypsoify/style.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is compiled SASS code. */
+body,#wp-content-editor-tools{background:#f6f6f6}#wpwrap{top:14px}#wp-admin-bar-notes #wpnt-notes-unread-count.wpn-unread{background-image:none !important;background-color:#ff76b8 !important;border:none !important}#adminmenu #collapse-menu,#adminmenu .wp-menu-separator,#screen-meta-links,.wp-submenu,#toplevel_page_jetpack{display:none}.wp-menu-open .wp-submenu{display:block}#adminmenuwrap,#adminmenuback,#adminmenu{background:#fff}#adminmenuback{border-right:1px solid #e1e2e2}#adminmenu,#adminmenuwrap,#adminmenuback,#adminmenu .wp-submenu{width:272px}#adminmenu{margin-top:71px}#adminmenu .wp-has-current-submenu .wp-submenu,#adminmenu .opensub .wp-submenu,#adminmenu .opensub .wp-submenu:after,#adminmenu a.wp-has-current-submenu:focus+.wp-submenu{background:transparent !important}#adminmenu li.menu-top:hover,#adminmenu li.opensub>a.menu-top,#adminmenu li>a.menu-top:focus,#adminmenu li.wp-menu-open,#adminmenu a:hover{background:#f6f6f6}#adminmenu .wp-submenu-head,#adminmenu a.menu-top{padding:5px 0 5px 5px}#adminmenu .wp-has-current-submenu ul>li>a{padding:11px 16px 11px 20px;font-size:14px}#adminmenu .wp-submenu a:hover{background-color:#e1e2e2}#adminmenu>li.wp-first-item{border-bottom:1px solid rgba(200,215,225,0.5)}#adminmenu a.wp-has-current-submenu:after,#adminmenu>li.current>a.current:after,#adminmenu li.wp-has-submenu.wp-not-current-submenu.opensub:hover:after{border:none}#adminmenu .dashicons,#adminmenu .dashicons-before:before{width:24px;height:24px;font-size:24px}#adminmenu a,#adminmenu div.wp-menu-image:before{color:#3d4145 !important}#adminmenu li.current a.menu-top,#adminmenu li.wp-has-current-submenu a.wp-has-current-submenu{background:#d8dee4}#adminmenu div.wp-menu-image.svg{filter:brightness(0.25)}#adminmenu li.wp-menu-open div.wp-menu-image.svg{filter:brightness(100)}#adminmenu li.wp-menu-open div.wp-menu-image:before,#adminmenu li.wp-menu-open div.wp-menu-name{color:#016087 !important}#adminmenu div.wp-menu-name{color:#2b2d2f;font-size:15px;padding:9px 0 8px 41px}#adminmenu li.menu-top{min-height:46px}#adminmenu .awaiting-mod,#adminmenu .update-plugins{background-color:#016087}.no-js li.wp-has-current-submenu:hover .wp-submenu{background:transparent !important}#wpcontent,#wpfooter{margin-left:272px}#toplevel_page_plugins div.wp-menu-image.svg,#toplevel_page_plugin-install div.wp-menu-image.svg{background-size:24px auto}#toplevel_page_plugins div.wp-menu-image.svg{position:relative;left:-2px}#calypso-sidebar-header{position:fixed;top:47px;left:0;width:272px;height:70px;background:#fff;z-index:10000}#calypso-sidebar-header svg{float:left;position:relative;left:10px;top:23px}#calypso-sidebar-header ul{float:left;position:relative;top:3px;left:15px}#calypso-sidebar-header ul li{margin:0}#calypso-sidebar-header ul li#calypso-sitename{font-size:12px;color:#636d75;overflow:hidden;white-space:nowrap;width:225px}@media screen and (max-width: 782px){#calypso-sidebar-header ul li#calypso-sitename{width:150px}}#calypso-sidebar-header ul li#calypso-sitename:after{content:'';display:block;position:absolute;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;background:linear-gradient(to right, rgba(255,255,255,0), #fff 90%);top:0px;bottom:0px;right:0px;left:auto;width:20%;height:auto}#calypso-sidebar-header ul li#calypso-plugins{font-weight:bold;color:#2b2d2f;font-size:16px}.folded #adminmenu .wp-has-current-submenu .wp-submenu,.folded #adminmenu .opensub .wp-submenu,.folded #adminmenu .opensub .wp-submenu:after,.folded #adminmenu a.wp-has-current-submenu:focus+.wp-submenu{background:#f6f6f6 !important}.folded #adminmenu li.menu-top .wp-submenu>li>a{padding:7px 12px 7px 46px;font-size:14px}.folded #adminmenu li.current.menu-top,.folded #adminmenu li.wp-has-current-submenu,.folded #adminmenu li.wp-has-current-submenu .wp-submenu .wp-submenu-head{background:#f3f5f6}.folded #adminmenu .wp-submenu .wp-submenu-head{padding:14px 4px 14px 11px}.folded #adminmenu a.menu-top{padding-left:1px}.folded #wpcontent #calypso-sidebar-header{width:36px}.folded #wpcontent #calypso-sidebar-header svg{left:6px}.folded #wpcontent #calypso-sidebar-header ul{display:none}.folded .no-js li.wp-has-current-submenu:hover .wp-submenu{background:#f6f6f6 !important}.folded #toplevel_page_plugins div.wp-menu-image.svg{position:relative;left:-2px}@media only screen and (max-width: 960px){#calypso-sidebar-header{width:36px}#calypso-sidebar-header ul{display:none}#calypso-sidebar-header svg{left:6px}#adminmenu a.menu-top{padding-left:1px}}@media screen and (max-width: 782px){#calypso-sidebar-header{position:absolute;display:none;width:190px;top:-14px}.wp-responsive-open #calypso-sidebar-header{display:block}#calypso-sidebar-header ul{display:block}.auto-fold #adminmenu .wp-menu-name{margin-left:0}.auto-fold #adminmenu{top:-14px}.auto-fold #adminmenu .selected,#adminmenu li.opensub>a.menu-top,#adminmenu li>a.menu-top:focus,#adminmenu li.wp-menu-open{background:#d8dee4 !important}#adminmenu .wp-submenu,.auto-fold #adminmenu .selected .wp-submenu,.auto-fold #adminmenu .wp-menu-open .wp-submenu,#adminmenu a.wp-has-current-submenu:focus+.wp-submenu{background:#fff !important}.auto-fold #adminmenu li.selected div.wp-menu-image.svg{filter:brightness(100)}.auto-fold #adminmenu li.selected div.wp-menu-image:before,.auto-fold #adminmenu li.selected div.wp-menu-name{color:#016087 !important}#wpadminbar .quicklinks>ul>li>a,#wpadminbar .quicklinks>ul>li>.ab-empty-item{padding:0 15px !important}#wpadminbar li#wp-admin-bar-ab-new-post a{padding:7px 15px !important}}@media screen and (max-width: 600px){#calypso-sidebar-header{top:32px}.auto-fold #adminmenu{top:32px}}@media screen and (max-width: 480px){#wpadminbar #wp-admin-bar-blog.my-sites>a.ab-item:before{margin-top:4px !important}#wpadminbar #wp-admin-bar-newdash>a.ab-item:before{margin-top:6px !important}#wpadminbar ul li#wp-admin-bar-ab-new-post a:before{top:-5px !important;margin-left:-12px !important}}.nav-tab-wrapper,.wrap h2.nav-tab-wrapper{margin:10px 0 25px;background:#fff;border:1px solid rgba(200,215,225,0.5)}.nav-tab{border:none;background:none;font-weight:400;padding:3px 13px 12px;color:#016087}.nav-tab-active,.nav-tab-active:focus,.nav-tab-active:focus:active,.nav-tab-active:hover{background:transparent;box-shadow:none}.nav-tab:first-child{margin-left:0}.nav-tab-active,.nav-tab-active:focus,.nav-tab-active:focus:active{border-bottom:2px solid #3d4145;color:#2b2d2f}#wpadminbar{background:#016087;-webkit-box-shadow:none;-mozilla-box-shadow:none;height:46px;position:fixed}#wpadminbar .ab-top-menu>li>.ab-item{font-size:14px}#wpadminbar .ab-top-menu>li.hover>.ab-item{background:#6f93ad !important;color:#fff}#wpadminbar *{line-height:46px}#wpadminbar .quicklinks a,#wpadminbar .quicklinks .ab-empty-item,#wpadminbar .shortlink-input{height:46px}#wpadminbar .quicklinks>ul>li>a{padding:0 15px}#wpadminbar .quicklinks>ul>li.current>a{background:#004966}#wpadminbar:not(.mobile) .ab-top-menu>li>.ab-item:focus,#wpadminbar.nojq .quicklinks .ab-top-menu>li>.ab-item:focus,#wpadminbar .ab-top-menu>li.ab-hover>.ab-item{background:transparent !important}#wpadminbar:not(.mobile) .ab-top-menu>li:hover>.ab-item{background:#6f93ad !important;color:#fff}#wpadminbar .ab-top-menu>li.my-sites>.ab-item,#wpadminbar .ab-top-menu>li.my-sites.hover>.ab-item,#wpadminbar .ab-top-menu>li.my-sites.ab-hover>.ab-item{background:#004966 !important}#wpadminbar #wp-admin-bar-blog.my-sites>a.ab-item:before,#wpadminbar #wp-admin-bar-newdash>a.ab-item:before{margin-top:12px}#wpadminbar ul li#wp-admin-bar-ab-new-post{border-radius:3px}#wpadminbar ul li#wp-admin-bar-ab-new-post a{padding:6px 15px;color:#016087 !important}#wpadminbar ul li#wp-admin-bar-ab-new-post a span{color:#016087 !important;font-size:14px !important}#wpadminbar ul li#wp-admin-bar-ab-new-post a:before,#wpadminbar ul li#wp-admin-bar-ab-new-post a:after{background-image:url('data:image/svg+xml;utf8,<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect x="0" fill="none" width="24" height="24"/><g><path fill="%230087be" d="M21 14v5c0 1.105-.895 2-2 2H5c-1.105 0-2-.895-2-2V5c0-1.105.895-2 2-2h5v2H5v14h14v-5h2z"/><path fill="%230087be" d="M21 7h-4V3h-2v4h-4v2h4v4h2V9h4"/></g></svg>') !important}#wpadminbar ul li#wp-admin-bar-ab-new-post:hover,#wpadminbar ul li#wp-admin-bar-ab-new-post:hover>.ab-item{background:#f6f6f6 !important;opacity:1;border-radius:3px !important}#wpadminbar li#wp-admin-bar-blog.menupop>.ab-sub-wrapper,#wpadminbar li#wp-admin-bar-newdash.menupop>.ab-sub-wrapper,#wpadminbar li#wp-admin-bar-my-account.menupop>.ab-sub-wrapper{display:none !important}#wpadminbar li#wp-admin-bar-notes.active,#wpadminbar li#wp-admin-bar-notes.active>.ab-item{background:#004966 !important}#wpadminbar li#wp-admin-bar-notes>#wpnt-notes-panel2{top:46px}#wpadminbar .ab-top-menu>li.ab-active>.ab-item,#wpadminbar>#wp-toolbar .wpnt-show span.noticon,#wpadminbar #wp-admin-bar-notes.wpnt-show .noticon{color:#fff !important}#wpadminbar .ab-active>a.ab-item:before,#wpadminbar #wp-admin-bar-notes.active .noticon-bell:before{filter:brightness(100) !important}#wpadminbar .quicklinks>ul>li#wp-admin-bar-notes>a.ab-item span.noticon,#wpadminbar>#wp-toolbar span.noticon,#wpadminbar #wp-admin-bar-notes .noticon{top:10px}#wpadminbar>#wp-toolbar>#wp-admin-bar-root-default .ab-icon,#wpadminbar .ab-icon,#wpadminbar .ab-item:before{font-size:24px;line-height:1.45}.wrap{margin:20px 30px 25px 15px}@media screen and (max-width: 782px){.wrap{margin:10px 18px 10px 7px}}.subsubsub,.wp-filter{margin:10px 0 25px;background:#fff;border:1px solid rgba(200,215,225,0.5);width:100%;box-shadow:none;padding:0}.subsubsub a,.filter-links li>a{padding:10px 15px;display:inline-block;font-size:14px;margin:0;color:#016087;border-bottom:2px solid #fff;outline:none}.subsubsub a:focus,.filter-links li>a:focus{box-shadow:0 0 0 1px #016087,0 0 2px 1px #6f93ad}.subsubsub a:hover,.filter-links li>a:hover{color:#23354b;background-color:#f3f5f6}.subsubsub a:hover:not(.current),.filter-links li>a:hover:not(.current){border-color:#f3f5f6}.filter-links li>a{padding:16px}.subsubsub a.current,.filter-links .current{border-bottom:2px solid #3d4145}.count{display:inline-block;padding:1px 6px;border:solid 1px #969ca1;border-radius:12px;font-size:11px;font-weight:bold;line-height:14px;color:#636d75;text-align:center;margin-left:2px}.plugins-php .plugins a{color:#016087}.plugins-php .plugins a:hover,.plugins-php .plugins a:focus{color:#23354b}.plugins-php .plugins a:focus{box-shadow:0 0 0 1px #016087,0 0 2px 1px #6f93ad}.plugins-php .plugins a.delete{color:#eb0001}.plugins-php .plugins a.delete:hover,.plugins-php .plugins a.delete:focus{color:#ac120b}.plugins-php .plugins a.delete:focus{box-shadow:0 0 0 1px #eb0001,0 0 2px 1px #ff8248}.plugins-php .tablenav{clear:none;float:left;margin-bottom:15px}.plugins-php .tablenav .one-page .displaying-num{display:none}.plugins-php .bulkactions select:focus{border-color:#016087;box-shadow:0 0 2px #6f93ad}.plugins-php p.search-box{margin-top:5px}.plugins-php p.search-box .wp-filter-search:focus{border-color:#016087;box-shadow:0 0 2px #6f93ad}.plugins-php .plugin-update-tr.active td,.plugins-php .plugins .active th{border-left:4px solid #016087}.plugins-php .plugins .active th,.plugins-php .plugins .active td,.plugins-php .plugins .active th.check-column,.plugins-php .plugin-update-tr.active td{background-color:#f3f5f6}.wrap .wp-heading-inline+.page-title-action,.wrap .add-new-h2,.wrap .add-new-h2:active,.wrap .page-title-action,.wrap .page-title-action:active{background:#d52c82;border-color:#992053;color:#fff;border-style:solid;border-width:1px 1px 2px;cursor:pointer;display:inline-block;margin:0 5px 0 0;outline:0;overflow:hidden;font-weight:500;text-overflow:ellipsis;text-decoration:none;vertical-align:middle;box-sizing:border-box;font-size:13px;line-height:21px;border-radius:4px;padding:2px 10px 2px;margin-bottom:2px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.wrap .wp-heading-inline+.page-title-action:hover,.wrap .add-new-h2:hover,.wrap .add-new-h2:active:hover,.wrap .page-title-action:hover,.wrap .page-title-action:active:hover{background-color:#ff3997}.wrap .wp-heading-inline+.page-title-action:focus,.wrap .add-new-h2:focus,.wrap .add-new-h2:active:focus,.wrap .page-title-action:focus,.wrap .page-title-action:active:focus{box-shadow:0 0 0 2px #ff76b8;background-color:#ff3997}.wp-core-ui .button{background:#fff}.wp-core-ui .button:focus{border-color:#016087;box-shadow:0 0 3px #6f93ad}.wp-core-ui .button-primary{background:#016087;border-color:#23354b;color:#fff;text-shadow:none}.wp-core-ui .button-primary:hover,.wp-core-ui .button-primary:focus{background-color:#46799a}.ui-tabs-nav li,.wp-switch-editor{background-color:#f6f6f6 !important}.plugin-card a,.popular-tags a,.filter-links>li>a{color:#016087}.plugin-card a:hover,.plugin-card a:focus,.popular-tags a:hover,.popular-tags a:focus,.filter-links>li>a:hover,.filter-links>li>a:focus{color:#23354b}.plugin-card a:focus,.popular-tags a:focus,.filter-links>li>a:focus{box-shadow:0 0 0 1px #016087,0 0 2px 1px #6f93ad}.plugin-card-bottom,.alternate,.striped>tbody>:nth-child(odd),ul.striped>:nth-child(odd),.ui-tabs-panel,.ui-tabs-nav li.ui-tabs-active,.ui-tabs-nav li.ui-tabs-active:hover,div.mce-toolbar-grp,.html-active .switch-html,.tmce-active .switch-tmce,#post-status-info,.quicktags-toolbar,#major-publishing-actions{background-color:#fff;border-color:#d7e1e9}.wp-filter .search-form{margin-right:10px}.form-toggle[type="checkbox"]{display:none}.form-toggle__switch{position:relative;display:inline-block;border-radius:12px;box-sizing:border-box;padding:2px;width:40px;height:24px;vertical-align:middle;align-self:flex-start;outline:0;cursor:pointer;transition:all .4s ease, box-shadow 0s}.form-toggle__switch:before,.form-toggle__switch:after{position:relative;display:block;content:"";width:20px;height:20px}.form-toggle__switch:after{left:0;border-radius:50%;background:#fff;transition:all .2s ease}.form-toggle__switch:before{display:none}.accessible-focus .form-toggle__switch:focus{box-shadow:0 0 0 2px #016087}.form-toggle__label{cursor:pointer}.is-disabled .form-toggle__label{cursor:default}.form-toggle__label .form-toggle__label-content{flex:0 1 100%;margin-left:12px}.accessible-focus .form-toggle:focus+.form-toggle__label .form-toggle__switch{box-shadow:0 0 0 2px #016087}.accessible-focus .form-toggle:focus:checked+.form-toggle__label .form-toggle__switch{box-shadow:0 0 0 2px #6f93ad}.form-toggle+.form-toggle__label .form-toggle__switch{background:#b0b5b8}.form-toggle:not(:disabled)+.form-toggle__label:hover .form-toggle__switch{background:#ccced0}.form-toggle:checked+.form-toggle__label .form-toggle__switch{background:#016087}.form-toggle:checked+.form-toggle__label .form-toggle__switch:after{left:16px}.form-toggle:checked:not(:disabled)+.form-toggle__label:hover .form-toggle__switch{background:#6f93ad}.form-toggle:disabled+label.form-toggle__label span.form-toggle__switch{opacity:0.25;cursor:default}.form-toggle.is-toggling+.form-toggle__label .form-toggle__switch{background:#016087}.form-toggle.is-toggling:checked+.form-toggle__label .form-toggle__switch{background:#ccced0}.form-toggle.is-compact+.form-toggle__label .form-toggle__switch{border-radius:8px;width:24px;height:16px}.form-toggle.is-compact+.form-toggle__label .form-toggle__switch:before,.form-toggle.is-compact+.form-toggle__label .form-toggle__switch:after{width:12px;height:12px}.form-toggle.is-compact:checked+.form-toggle__label .form-toggle__switch:after{left:8px}
diff --git a/plugins/jetpack/modules/carousel.php b/plugins/jetpack/modules/carousel.php
new file mode 100644
index 00000000..fda08752
--- /dev/null
+++ b/plugins/jetpack/modules/carousel.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * Module Name: Carousel
+ * Module Description: Display images and galleries in a gorgeous, full-screen browsing experience
+ * Sort Order: 22
+ * Recommendation Order: 12
+ * First Introduced: 1.5
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Photos and Videos
+ * Feature: Appearance
+ * Additional Search Queries: gallery, carousel, diaporama, slideshow, images, lightbox, exif, metadata, image
+ */
+
+include dirname( __FILE__ ) . '/carousel/jetpack-carousel.php';
diff --git a/plugins/jetpack/modules/carousel/images/arrows-2x.png b/plugins/jetpack/modules/carousel/images/arrows-2x.png
new file mode 100644
index 00000000..01214f2e
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/images/arrows-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/carousel/images/arrows.png b/plugins/jetpack/modules/carousel/images/arrows.png
new file mode 100644
index 00000000..9251dce1
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/images/arrows.png
Binary files differ
diff --git a/plugins/jetpack/modules/carousel/images/carousel-likereblog-2x.png b/plugins/jetpack/modules/carousel/images/carousel-likereblog-2x.png
new file mode 100644
index 00000000..1dd594fe
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/images/carousel-likereblog-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/carousel/images/carousel-likereblog.png b/plugins/jetpack/modules/carousel/images/carousel-likereblog.png
new file mode 100644
index 00000000..e4cd0596
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/images/carousel-likereblog.png
Binary files differ
diff --git a/plugins/jetpack/modules/carousel/images/carousel-link-2x.png b/plugins/jetpack/modules/carousel/images/carousel-link-2x.png
new file mode 100644
index 00000000..9939ecba
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/images/carousel-link-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/carousel/images/carousel-link.png b/plugins/jetpack/modules/carousel/images/carousel-link.png
new file mode 100644
index 00000000..225348db
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/images/carousel-link.png
Binary files differ
diff --git a/plugins/jetpack/modules/carousel/images/carousel-sprite-2x.png b/plugins/jetpack/modules/carousel/images/carousel-sprite-2x.png
new file mode 100644
index 00000000..85d0e3f3
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/images/carousel-sprite-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/carousel/images/carousel-sprite.png b/plugins/jetpack/modules/carousel/images/carousel-sprite.png
new file mode 100644
index 00000000..41ad9c8d
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/images/carousel-sprite.png
Binary files differ
diff --git a/plugins/jetpack/modules/carousel/jetpack-carousel-rtl.css b/plugins/jetpack/modules/carousel/jetpack-carousel-rtl.css
new file mode 100644
index 00000000..bf71e026
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/jetpack-carousel-rtl.css
@@ -0,0 +1 @@
+.jp-carousel-wrap *{line-height:inherit}.jp-carousel-overlay{background:#000}div.jp-carousel-fadeaway{background:-moz-linear-gradient(bottom,rgba(0,0,0,.5),rgba(0,0,0,0));background:-webkit-gradient(linear,right bottom,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,0)));position:fixed;bottom:0;z-index:2147483647;width:100%;height:15px}.jp-carousel-next-button span,.jp-carousel-previous-button span{background:url(../modules/carousel/images/arrows.png) no-repeat center center;background-size:200px 126px}.jp-carousel-msg{font-family:"Open Sans",sans-serif;font-style:normal;display:inline-block;line-height:19px;padding:11px 15px;font-size:14px;text-align:center;margin:25px 2px 0 20px;background-color:#fff;border-right:4px solid #ffba00;box-shadow:0 1px 1px 0 rgba(0,0,0,.1)}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (-o-min-device-pixel-ratio:3/2),only screen and (min--moz-device-pixel-ratio:1.5),only screen and (min-device-pixel-ratio:1.5){.jp-carousel-next-button span,.jp-carousel-previous-button span{background-image:url(../modules/carousel/images/arrows-2x.png)}}.jp-carousel-wrap{font-family:"Helvetica Neue",sans-serif!important}.jp-carousel-info{position:absolute;bottom:0;text-align:right!important;-webkit-font-smoothing:subpixel-antialiased!important}.jp-carousel-info ::selection{background:#68c9e8;color:#fff}.jp-carousel-info ::-moz-selection{background:#68c9e8;color:#fff}.jp-carousel-photo-info{position:relative;right:25%;width:50%}.jp-carousel-transitions .jp-carousel-photo-info{transition:.4s ease-out}.jp-carousel-info h2{background:100% 0!important;border:none!important;color:#999;display:block!important;font:normal 13px/1.25em "Helvetica Neue",sans-serif!important;letter-spacing:0!important;margin:7px 0 0 0!important;padding:10px 0 0!important;overflow:hidden;text-align:right;text-shadow:none!important;text-transform:none!important;-webkit-font-smoothing:subpixel-antialiased}.jp-carousel-next-button,.jp-carousel-previous-button{text-indent:-9999px;overflow:hidden;cursor:pointer}.jp-carousel-next-button span,.jp-carousel-previous-button span{position:absolute;top:0;bottom:0;width:82px;zoom:1;opacity:.2}.jp-carousel-transitions .jp-carousel-next-button span,.jp-carousel-transitions .jp-carousel-previous-button span{transition:.5s opacity ease-out}.jp-carousel-next-button:hover span,.jp-carousel-previous-button:hover span{opacity:.6}.jp-carousel-next-button span{background-position:-110px center;left:0}.jp-carousel-previous-button span{background-position:-10px center;right:0}.jp-carousel-buttons{margin:-18px -20px 15px;padding:8px 10px;border-bottom:1px solid #222;background:#222;text-align:center}div.jp-carousel-buttons a{border:none!important;color:#999;font:normal 11px/1.2em "Helvetica Neue",sans-serif!important;letter-spacing:0!important;padding:5px 0 5px 2px;text-decoration:none!important;text-shadow:none!important;vertical-align:middle;-webkit-font-smoothing:subpixel-antialiased}div.jp-carousel-buttons a:hover{color:#68c9e8;border:none!important}.jp-carousel-transitions div.jp-carousel-buttons a:hover{transition:none!important}.jp-carousel-next-button,.jp-carousel-previous-button,.jp-carousel-slide,.jp-carousel-slide img{-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0)}.jp-carousel-slide{position:fixed;width:0;bottom:0;background-color:#000;border-radius:2px;-webkit-border-radius:2px;-moz-border-radius:2px;-ms-border-radius:2px;-o-border-radius:2px}.jp-carousel-transitions .jp-carousel-slide{transition:.3s ease-out}.jp-carousel-slide.selected{position:absolute!important;opacity:1}.jp-carousel-slide{opacity:.25}.jp-carousel-slide img{display:block;width:100%!important;height:100%!important;max-width:100%!important;max-height:100%!important;background:100% 0!important;border:none!important;padding:0!important;box-shadow:0 2px 8px rgba(0,0,0,.1);zoom:1}.jp-carousel-transitions .jp-carousel-slide{transition:opacity .4s linear}.jp-carousel-close-hint{color:#999;cursor:default;letter-spacing:0!important;padding:.35em 0 0;position:absolute;text-align:left;width:90%}.jp-carousel-transitions .jp-carousel-close-hint{transition:color .2s linear}.jp-carousel-close-hint span{cursor:pointer;background-color:#000;background-color:rgba(0,0,0,.8);display:inline-block;height:22px;font:400 24px/1 "Helvetica Neue",sans-serif!important;line-height:22px;margin:0 .4em 0 0;text-align:center;vertical-align:middle;width:22px;border-radius:4px}.jp-carousel-transitions .jp-carousel-close-hint span{transition:border-color .2s linear}.jp-carousel-close-hint:hover{cursor:default;color:#fff}.jp-carousel-close-hint:hover span{border-color:#fff}a.jp-carousel-image-download,div.jp-carousel-buttons a.jp-carousel-commentlink,div.jp-carousel-buttons a.jp-carousel-reblog{background:url(../modules/carousel/images/carousel-sprite.png?5) no-repeat;background-size:16px 200px}div.jp-carousel-buttons a.jp-carousel-commentlink,div.jp-carousel-buttons a.jp-carousel-reblog{margin:0 0 0 14px!important}div.jp-carousel-buttons a.jp-carousel-reblog.reblogged{background-color:#303030;padding-left:8px!important;border-radius:2px;border-radius:2px;-webkit-border-radius:2px;-moz-border-radius:2px;-ms-border-radius:2px;-o-border-radius:2px}div.jp-carousel-buttons a.jp-carousel-reblog.reblogged{margin:0 -12px 0 2px!important}div.jp-carousel-buttons a.jp-carousel-reblog,div.jp-carousel-buttons a.jp-carousel-reblog.reblogged:hover{background-position:6px -36px;padding-left:auto!important;padding-right:26px!important;color:#999}div.jp-carousel-buttons a.jp-carousel-commentlink{background-position:100% -156px;padding-right:19px!important}div.jp-carousel-buttons a.jp-carousel-reblog.reblogged:hover{cursor:default}div.jp-carousel-buttons a.jp-carousel-reblog:hover{background-position:6px -56px;color:#68c9e8}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (-o-min-device-pixel-ratio:3/2),only screen and (min--moz-device-pixel-ratio:1.5),only screen and (min-device-pixel-ratio:1.5){a.jp-carousel-image-download,div.jp-carousel-buttons a.jp-carousel-commentlink,div.jp-carousel-buttons a.jp-carousel-reblog{background-image:url(../modules/carousel/images/carousel-sprite-2x.png?5)}}div#carousel-reblog-box{background:#222;background:-moz-linear-gradient(bottom,#222,#333);background:-webkit-gradient(linear,right bottom,right top,from(#222),to(#333));padding:3px 0 0;display:none;margin:5px auto 0;border-radius:2px;box-shadow:0 0 20px rgba(0,0,0,.9);height:74px;width:565px}#carousel-reblog-box textarea{background:#999;font:13px/1.4 "Helvetica Neue",sans-serif!important;color:#444;padding:3px 6px;width:370px;height:48px;float:right;margin:6px 9px 0 9px;border:1px solid #666;box-shadow:inset -2px 2px 2px rgba(0,0,0,.2);border-radius:2px}#carousel-reblog-box textarea:focus{background:#ccc;color:#222}#carousel-reblog-box label{color:#aaa;font-size:11px;padding-left:2px;padding-right:2px;display:inline;font-weight:400}#carousel-reblog-box select{width:110px;padding:0;font-size:12px;font-family:"Helvetica Neue",sans-serif!important;background:#333;color:#eee;border:1px solid #444;margin-top:5px}#carousel-reblog-box .submit,#wrapper #carousel-reblog-box p.response{float:right;width:154px;padding-top:0;padding-right:1px;overflow:hidden;height:34px;margin:3px 2px 0 0!important}#wrapper #carousel-reblog-box p.response{font-size:13px;clear:none;padding-right:2px;height:34px;color:#aaa}#carousel-reblog-box input#carousel-reblog-submit,#jp-carousel-comment-form-button-submit{font:13px/24px "Helvetica Neue",sans-serif!important;margin-top:8px;padding:0 10px!important;border-radius:1em;height:24px;color:#333;cursor:pointer;font-weight:400;background:#aaa;background:-moz-linear-gradient(bottom,#aaa,#ccc);background:-webkit-gradient(linear,right bottom,right top,from(#aaa),to(#ccc));border:1px solid #444}#carousel-reblog-box input#carousel-reblog-submit:hover,#jp-carousel-comment-form-button-submit:hover{background:#ccc;background:-moz-linear-gradient(bottom,#ccc,#eee);background:-webkit-gradient(linear,right bottom,right top,from(#ccc),to(#eee))}#carousel-reblog-box .canceltext{color:#aaa;font-size:11px;line-height:24px}#carousel-reblog-box .canceltext a{color:#fff}.jp-carousel-titleanddesc{border-top:1px solid #222;color:#999;font-size:15px;padding-top:24px;margin-bottom:20px;font-weight:400}.jp-carousel-titleanddesc-title{font:300 1.5em/1.1 "Helvetica Neue",sans-serif!important;text-transform:none!important;color:#fff;margin:0 0 15px;padding:0}.jp-carousel-titleanddesc-desc p{color:#999;line-height:1.4;margin-bottom:.75em}.jp-carousel-comments p a,.jp-carousel-info h2 a,.jp-carousel-titleanddesc p a{color:#fff!important;border:none!important;text-decoration:underline!important;font-weight:400!important;font-style:normal!important}.jp-carousel-titleanddesc p b,.jp-carousel-titleanddesc p strong{font-weight:700;color:#999}.jp-carousel-titleanddesc p em,.jp-carousel-titleanddesc p i{font-style:italic;color:#999}.jp-carousel-comments p a:hover,.jp-carousel-info h2 a:hover,.jp-carousel-titleanddesc p a:hover{color:#68c9e8!important}.jp-carousel-titleanddesc p:empty{display:none}.jp-carousel-left-column-wrapper h1:after,.jp-carousel-left-column-wrapper h1:before,.jp-carousel-photo-info h1:after,.jp-carousel-photo-info h1:before{content:none!important}.jp-carousel-image-meta{background:#111;border:1px solid #222;color:#fff;font-size:13px;font:12px/1.4 "Helvetica Neue",sans-serif!important;overflow:hidden;padding:18px 20px;width:209px!important}.jp-carousel-image-meta h5,.jp-carousel-image-meta li{font-family:"Helvetica Neue",sans-serif!important;position:inherit!important;top:auto!important;left:auto!important;right:auto!important;bottom:auto!important;background:100% 0!important;border:none!important;font-weight:400!important;line-height:1.3em!important}.jp-carousel-image-meta ul{margin:0!important;padding:0!important;list-style:none!important}.jp-carousel-image-meta li{width:48%!important;display:inline-block!important;vertical-align:top!important;margin:0 0 15px 2%!important;color:#fff!important;font-size:13px!important}.jp-carousel-image-meta h5{color:#999!important;text-transform:uppercase!important;font-size:10px!important;margin:0 0 2px!important;letter-spacing:.1em!important}a.jp-carousel-image-download{padding-right:23px;display:inline-block;clear:both;color:#999;line-height:1;font-weight:400;font-size:13px;text-decoration:none;background-position:100% -82px}a.jp-carousel-image-download span.photo-size{font-size:11px;border-radius:1em;margin-right:2px;display:inline-block}a.jp-carousel-image-download span.photo-size-times{padding:0 2px 0 1px}a.jp-carousel-image-download:hover{background-position:100% -122px;color:#68c9e8;border:none!important}.jp-carousel-image-map{position:relative;margin:-20px -20px 20px;border-bottom:1px solid rgba(255,255,255,.17);height:154px}.jp-carousel-image-map img.gmap-main{border-top-right-radius:6px;border-left:1px solid rgba(255,255,255,.17)}.jp-carousel-image-map div.gmap-topright{width:94px;height:154px;position:absolute;top:0;left:0}.jp-carousel-image-map div.imgclip{overflow:hidden;border-top-left-radius:6px}.jp-carousel-image-map div.gmap-topright img{margin-right:-40px}.jp-carousel-image-map img.gmap-bottomright{position:absolute;top:96px;left:0}.jp-carousel-comments{font:15px/1.7 "Helvetica Neue",sans-serif!important;font-weight:400;background:none transparent}.jp-carousel-comments p a:active,.jp-carousel-comments p a:focus,.jp-carousel-comments p a:hover{color:#68c9e8!important}.jp-carousel-comment{background:none transparent;color:#999;margin-bottom:20px;clear:right;overflow:auto;width:100%}.jp-carousel-comment p{color:#999!important}.jp-carousel-comment .comment-author{font-size:13px;font-weight:400;padding:0;width:auto;display:inline;float:none;border:none;margin:0}.jp-carousel-comment .comment-author a{color:#fff}.jp-carousel-comment .comment-gravatar{float:right}.jp-carousel-comment .comment-content{border:none;margin-right:85px;padding:0}.jp-carousel-comment .avatar{margin:0 0 0 20px;border-radius:4px;border:none!important;padding:0!important;background-color:transparent!important}.jp-carousel-comment .comment-date{color:#999;margin-top:4px;font-size:11px;display:inline;float:left}#jp-carousel-comment-form{margin:0 0 10px!important;float:right;width:100%}textarea#jp-carousel-comment-form-comment-field{background:rgba(34,34,34,.9);border:1px solid #3a3a3a;color:#aaa;font:15px/1.4 "Helvetica Neue",sans-serif!important;width:100%;padding:10px 10px 5px;margin:0;float:none;height:147px;box-shadow:inset -2px 2px 2px rgba(0,0,0,.2);border-radius:3px;overflow:hidden;box-sizing:border-box}textarea#jp-carousel-comment-form-comment-field::-webkit-input-placeholder{color:#555}textarea#jp-carousel-comment-form-comment-field:focus{background:#ccc;color:#222}textarea#jp-carousel-comment-form-comment-field:focus::-webkit-input-placeholder{color:#aaa}#jp-carousel-comment-form-spinner{color:#fff;margin:22px 10px 0 0;display:block;width:20px;height:20px;float:right}#jp-carousel-comment-form-submit-and-info-wrapper{display:none;overflow:hidden;width:100%}#jp-carousel-comment-form-commenting-as input{background:rgba(34,34,34,.9);border:1px solid #3a3a3a;color:#aaa;font:13px/1.4 "Helvetica Neue",sans-serif!important;padding:3px 6px;float:right;box-shadow:inset -2px 2px 2px rgba(0,0,0,.2);border-radius:2px;width:285px}#jp-carousel-comment-form-commenting-as input:focus{background:#ccc;color:#222}#jp-carousel-comment-form-commenting-as p{font:400 13px/1.7 "Helvetica Neue",sans-serif!important;margin:22px 0 0;float:right}#jp-carousel-comment-form-commenting-as fieldset{float:right;border:none;margin:20px 0 0 0;padding:0}#jp-carousel-comment-form-commenting-as fieldset{clear:both}#jp-carousel-comment-form-commenting-as label{font:400 13px/1.7 "Helvetica Neue",sans-serif!important;margin:0 0 3px 20px;float:right;width:100px}#jp-carousel-comment-form-button-submit{margin-top:20px;float:left}#js-carousel-comment-form-container{margin-bottom:15px;overflow:auto;width:100%}#jp-carousel-comment-form-container{margin-bottom:15px;overflow:auto;width:100%}#jp-carousel-comment-post-results{display:none;overflow:auto;width:100%}#jp-carousel-comment-post-results span{display:block;text-align:center;margin-top:20px;width:100%;overflow:auto;padding:1em 0;box-sizing:border-box;background:rgba(0,0,0,.7);border-radius:2px;font:13px/1.4 "Helvetica Neue",sans-serif!important;border:1px solid rgba(255,255,255,.17);box-shadow:inset 0 0 5px 5px rgba(0,0,0,1)}.jp-carousel-comment-post-error{color:#df4926}#jp-carousel-comments-closed{display:none;color:#999}#jp-carousel-comments-loading{font:400 15px/1.7 "Helvetica Neue",sans-serif!important;display:none;color:#999;text-align:right;margin-bottom:20px}.jp-carousel-light .jp-carousel-overlay{background:#fff}.jp-carousel-light .jp-carousel-next-button:hover span,.jp-carousel-light .jp-carousel-previous-button:hover span{opacity:.8}.jp-carousel-light .jp-carousel-close-hint:hover,.jp-carousel-light .jp-carousel-titleanddesc div{color:#000!important}.jp-carousel-light .jp-carousel-comment .comment-author a,.jp-carousel-light .jp-carousel-comments p a,.jp-carousel-light .jp-carousel-info h2 a,.jp-carousel-light .jp-carousel-titleanddesc p a{color:#1e8cbe!important}.jp-carousel-light .jp-carousel-comment .comment-author a:hover,.jp-carousel-light .jp-carousel-comments p a:hover,.jp-carousel-light .jp-carousel-info h2 a:hover,.jp-carousel-light .jp-carousel-titleanddesc p a:hover{color:#f1831e!important}.jp-carousel-light .jp-carousel-comment,.jp-carousel-light .jp-carousel-comment p,.jp-carousel-light .jp-carousel-info h2,.jp-carousel-light .jp-carousel-titleanddesc,.jp-carousel-light .jp-carousel-titleanddesc p,.jp-carousel-light .jp-carousel-titleanddesc p b,.jp-carousel-light .jp-carousel-titleanddesc p em,.jp-carousel-light .jp-carousel-titleanddesc p i,.jp-carousel-light .jp-carousel-titleanddesc p strong,.jp-carousel-light div.jp-carousel-buttons a{color:#666}.jp-carousel-light .jp-carousel-buttons{border-bottom-color:#f0f0f0;background:#f5f5f5}.jp-carousel-light div.jp-carousel-buttons a:hover{text-decoration:none;color:#f1831e}.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog,.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog:hover{background-position:4px -56px;padding-right:24px!important}.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog.reblogged{background-color:#2ea2cc;color:#fff}.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-commentlink{background-position:100% -176px}.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog.reblogged{background-position:5px -36px}.jp-carousel-light div#carousel-reblog-box{background:#eee;background:-moz-linear-gradient(bottom,#ececec,#f7f7f7);background:-webkit-gradient(linear,right bottom,right top,from(#ececec),to(#f7f7f7));box-shadow:0 2px 10px rgba(0,0,0,.1);border:1px solid #ddd}.jp-carousel-light #carousel-reblog-box textarea{border:1px inset #ccc;color:#666;border:1px solid #cfcfcf;background:#fff}.jp-carousel-light #carousel-reblog-box .canceltext{color:#888}.jp-carousel-light #carousel-reblog-box .canceltext a{color:#666}.jp-carousel-light #carousel-reblog-box select{background:#eee;color:#333;border:1px solid #aaa}#jp-carousel-comment-form-button-submit,.jp-carousel-light #carousel-reblog-box input#carousel-reblog-submit{color:#333;background:#fff;background:-moz-linear-gradient(bottom,#ddd,#fff);background:-webkit-gradient(linear,right bottom,right top,from(#ddd),to(#fff));border:1px solid #aaa}.jp-carousel-light .jp-carousel-image-meta{background:#fafafa;border:1px solid #eee;border-top-color:#f5f5f5;border-right-color:#f5f5f5;color:#333}.jp-carousel-light .jp-carousel-image-meta li{color:#000!important}.jp-carousel-light .jp-carousel-close-hint{color:#ccc}.jp-carousel-light .jp-carousel-close-hint span{background-color:#fff;border-color:#ccc}.jp-carousel-light #jp-carousel-comment-form-comment-field::-webkit-input-placeholder{color:#aaa}.jp-carousel-light #jp-carousel-comment-form-comment-field:focus{color:#333}.jp-carousel-light #jp-carousel-comment-form-comment-field:focus::-webkit-input-placeholder{color:#ddd}.jp-carousel-light a.jp-carousel-image-download{background-position:100% -122px}.jp-carousel-light a.jp-carousel-image-download:hover{background-position:100% -122px;color:#f1831e}.jp-carousel-light textarea#jp-carousel-comment-form-comment-field{background:#fbfbfb;color:#333;border:1px solid #dfdfdf;box-shadow:inset -2px 2px 2px rgba(0,0,0,.1)}.jp-carousel-light #jp-carousel-comment-form-commenting-as input{background:#fbfbfb;border:1px solid #dfdfdf;color:#333;box-shadow:inset -2px 2px 2px rgba(0,0,0,.1)}.jp-carousel-light #jp-carousel-comment-form-commenting-as input:focus{background:#fbfbfb;color:#333}.jp-carousel-light #jp-carousel-comment-post-results span{background:#f7f7f7;border:1px solid #dfdfdf;box-shadow:inset 0 0 5px rgba(0,0,0,.05)}.jp-carousel-light .jp-carousel-slide{background-color:#fff}.jp-carousel-light .jp-carousel-titleanddesc{border-top:1px solid #eee}.jp-carousel-light .jp-carousel-fadeaway{background:-moz-linear-gradient(bottom,rgba(255,255,255,.75),rgba(255,255,255,0));background:-webkit-gradient(linear,right bottom,right top,from(rgba(255,255,255,.75)),to(rgba(255,255,255,0)))}@media only screen and (max-width:760px){.jp-carousel-info{margin:0 10px!important}.jp-carousel-next-button,.jp-carousel-previous-button{display:none!important}.jp-carousel-buttons{display:none!important}.jp-carousel-image-meta{float:none!important;width:100%!important;box-sizing:border-box}.jp-carousel-close-hint{font-weight:800!important;font-size:26px!important;position:fixed!important;top:-10px}.jp-carousel-slide img{opacity:1}.jp-carousel-wrap{background-color:#000}.jp-carousel-fadeaway{display:none}#jp-carousel-comment-form-container{display:none!important}.jp-carousel-titleanddesc{padding-top:0!important;border:none!important}.jp-carousel-titleanddesc-title{font-size:1em!important}.jp-carousel-left-column-wrapper{padding:0;width:100%!important}.jp-carousel-photo-info{right:0!important;width:100%!important}} \ No newline at end of file
diff --git a/plugins/jetpack/modules/carousel/jetpack-carousel.css b/plugins/jetpack/modules/carousel/jetpack-carousel.css
new file mode 100644
index 00000000..6eb89bac
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/jetpack-carousel.css
@@ -0,0 +1,1129 @@
+.jp-carousel-wrap * {
+ line-height:inherit; /* prevent declarations of line-height in the universal selector */
+}
+
+.jp-carousel-overlay {
+ background: #000;
+}
+
+div.jp-carousel-fadeaway {
+ background: -moz-linear-gradient(bottom, rgba(0,0,0,0.5), rgba(0,0,0,0));
+ background: -webkit-gradient(linear, left bottom, left top, from(rgba(0,0,0,0.5)), to(rgba(0,0,0,0)));
+ position: fixed;
+ bottom: 0;
+ z-index: 2147483647;
+ width: 100%;
+ height: 15px;
+}
+
+.jp-carousel-next-button span,
+.jp-carousel-previous-button span {
+ background: url(./images/arrows.png) no-repeat center center;
+ background-size: 200px 126px;
+}
+
+.jp-carousel-msg {
+ font-family: "Open Sans", sans-serif;
+ font-style: normal;
+ display: inline-block;
+ line-height: 19px;
+ padding: 11px 15px;
+ font-size: 14px;
+ text-align: center;
+ margin: 25px 20px 0 2px;
+ background-color: #fff;
+ border-left: 4px solid #ffba00;
+ -webkit-box-shadow: 0 1px 1px 0 rgba(0,0,0,0.1);
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,0.1);
+}
+
+@media
+only screen and (-webkit-min-device-pixel-ratio: 1.5),
+only screen and (-o-min-device-pixel-ratio: 3/2),
+only screen and (min--moz-device-pixel-ratio: 1.5),
+only screen and (min-device-pixel-ratio: 1.5) {
+ .jp-carousel-next-button span,
+ .jp-carousel-previous-button span {
+ background-image: url(./images/arrows-2x.png);
+ }
+}
+
+.jp-carousel-wrap {
+ font-family: "Helvetica Neue", sans-serif !important;
+}
+
+.jp-carousel-info {
+ position: absolute;
+ bottom: 0;
+ text-align: left !important;
+ -webkit-font-smoothing: subpixel-antialiased !important;
+}
+
+.jp-carousel-info ::selection {
+ background: #68c9e8; /* Safari */
+ color: #fff;
+ }
+
+.jp-carousel-info ::-moz-selection {
+ background: #68c9e8; /* Firefox */
+ color: #fff;
+}
+
+.jp-carousel-photo-info {
+ position: relative;
+ left: 25%;
+ width: 50%;
+}
+
+.jp-carousel-transitions .jp-carousel-photo-info {
+ -webkit-transition: 400ms ease-out;
+ -moz-transition: 400ms ease-out;
+ -o-transition: 400ms ease-out;
+ transition: 400ms ease-out;
+}
+
+.jp-carousel-info h2 {
+ background: none !important;
+ border: none !important;
+ color: #999;
+ display: block !important;
+ font: normal 13px/1.25em "Helvetica Neue", sans-serif !important;
+ letter-spacing: 0 !important;
+ margin: 7px 0 0 0 !important;
+ padding: 10px 0 0 !important;
+ overflow: hidden;
+ text-align: left;
+ text-shadow: none !important;
+ text-transform: none !important;
+ -webkit-font-smoothing: subpixel-antialiased;
+}
+
+.jp-carousel-next-button,
+.jp-carousel-previous-button {
+ text-indent: -9999px;
+ overflow: hidden;
+ cursor: pointer;
+}
+
+.jp-carousel-next-button span,
+.jp-carousel-previous-button span {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 82px;
+ zoom: 1;
+ filter: alpha(opacity=20);
+ opacity: 0.2;
+}
+
+.jp-carousel-transitions .jp-carousel-next-button span,
+.jp-carousel-transitions .jp-carousel-previous-button span {
+ -webkit-transition: 500ms opacity ease-out;
+ -moz-transition: 500ms opacity ease-out;
+ -o-transition: 500ms opacity ease-out;
+ transition: 500ms opacity ease-out;
+}
+
+.jp-carousel-next-button:hover span,
+.jp-carousel-previous-button:hover span {
+ filter: alpha(opacity=60);
+ opacity: 0.6;
+}
+.jp-carousel-next-button span {
+ background-position: -110px center;
+ right: 0;
+}
+
+.jp-carousel-previous-button span {
+ background-position: -10px center;
+ left:0;
+}
+
+.jp-carousel-buttons {
+ margin:-18px -20px 15px;
+ padding:8px 10px;
+ border-bottom:1px solid #222;
+ background: #222;
+ text-align: center;
+}
+
+div.jp-carousel-buttons a {
+ border: none !important;
+ color: #999;
+ font: normal 11px/1.2em "Helvetica Neue", sans-serif !important;
+ letter-spacing: 0 !important;
+ padding: 5px 2px 5px 0;
+ text-decoration: none !important;
+ text-shadow: none !important;
+ vertical-align: middle;
+ -webkit-font-smoothing: subpixel-antialiased;
+}
+
+div.jp-carousel-buttons a:hover {
+ color: #68c9e8;
+ border: none !important;
+}
+
+.jp-carousel-transitions div.jp-carousel-buttons a:hover {
+ -webkit-transition: none !important;
+ -moz-transition: none !important;
+ -o-transition: none !important;
+ transition: none !important;
+}
+
+.jp-carousel-slide, .jp-carousel-slide img, .jp-carousel-next-button,
+.jp-carousel-previous-button {
+ -webkit-transform:translate3d(0, 0, 0);
+ -moz-transform:translate3d(0, 0, 0);
+ -o-transform:translate3d(0, 0, 0);
+ -ms-transform:translate3d(0, 0, 0);
+}
+
+.jp-carousel-slide {
+ position:fixed;
+ width:0;
+ bottom:0;
+ background-color:#000;
+ border-radius:2px;
+ -webkit-border-radius:2px;
+ -moz-border-radius:2px;
+ -ms-border-radius:2px;
+ -o-border-radius:2px;
+}
+
+.jp-carousel-transitions .jp-carousel-slide {
+ -webkit-transition: 300ms ease-out;
+ -moz-transition: 300ms ease-out;
+ -o-transition: 300ms ease-out;
+ transition: 300ms ease-out;
+}
+
+.jp-carousel-slide.selected {
+ position: absolute !important;
+ filter: alpha(opacity=100);
+ opacity: 1;
+}
+
+.jp-carousel-slide {
+ filter: alpha(opacity=25);
+ opacity: 0.25;
+}
+
+.jp-carousel-slide img {
+ display: block;
+ width: 100% !important;
+ height: 100% !important;
+ max-width: 100% !important;
+ max-height: 100% !important;
+ background: none !important;
+ border: none !important;
+ padding: 0 !important;
+ -webkit-box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ -moz-box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ zoom: 1;
+}
+
+.jp-carousel-transitions .jp-carousel-slide {
+ -webkit-transition: opacity 400ms linear;
+ -moz-transition: opacity 400ms linear;
+ -o-transition: opacity 400ms linear;
+ transition: opacity 400ms linear;
+}
+
+.jp-carousel-close-hint {
+ color: #999;
+ cursor: default;
+ letter-spacing: 0 !important;
+ padding:0.35em 0 0;
+ position: absolute;
+ text-align: right;
+ width: 90%;
+}
+
+.jp-carousel-transitions .jp-carousel-close-hint {
+ -webkit-transition: color 200ms linear;
+ -moz-transition: color 200ms linear;
+ -o-transition: color 200ms linear;
+ transition: color 200ms linear;
+}
+
+.jp-carousel-close-hint span {
+ cursor: pointer;
+ background-color: black;
+ background-color: rgba(0,0,0,0.8);
+ display: inline-block;
+ height: 22px;
+ font: 400 24px/1 "Helvetica Neue", sans-serif !important;
+ line-height: 22px;
+ margin: 0 0 0 0.4em;
+ text-align: center;
+ vertical-align: middle;
+ width: 22px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.jp-carousel-transitions .jp-carousel-close-hint span {
+ -webkit-transition: border-color 200ms linear;
+ -moz-transition: border-color 200ms linear;
+ -o-transition: border-color 200ms linear;
+ transition: border-color 200ms linear;
+}
+
+.jp-carousel-close-hint:hover {
+ cursor: default;
+ color: #fff;
+}
+
+.jp-carousel-close-hint:hover span {
+ border-color: #fff;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog,
+div.jp-carousel-buttons a.jp-carousel-commentlink,
+a.jp-carousel-image-download {
+ background: url(./images/carousel-sprite.png?5) no-repeat;
+ background-size: 16px 200px;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog,
+div.jp-carousel-buttons a.jp-carousel-commentlink {
+ margin: 0 14px 0 0 !important;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog.reblogged {
+ background-color: #303030;
+ padding-right: 8px !important;
+ border-radius: 2px;
+ border-radius: 2px;
+ -webkit-border-radius:2px;
+ -moz-border-radius:2px;
+ -ms-border-radius:2px;
+ -o-border-radius:2px;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog.reblogged {
+ margin: 0 2px 0 -12px !important;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog,
+div.jp-carousel-buttons a.jp-carousel-reblog.reblogged:hover {
+ background-position: 6px -36px;
+ padding-right: auto !important;
+ padding-left: 26px !important;
+ color: #999;
+}
+
+div.jp-carousel-buttons a.jp-carousel-commentlink {
+ background-position: 0px -156px;
+ padding-left: 19px !important;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog.reblogged:hover {
+ cursor: default;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog:hover {
+ background-position: 6px -56px;
+ color: #68c9e8;
+}
+
+@media
+only screen and (-webkit-min-device-pixel-ratio: 1.5),
+only screen and (-o-min-device-pixel-ratio: 3/2),
+only screen and (min--moz-device-pixel-ratio: 1.5),
+only screen and (min-device-pixel-ratio: 1.5) {
+ div.jp-carousel-buttons a.jp-carousel-reblog,
+ div.jp-carousel-buttons a.jp-carousel-commentlink,
+ a.jp-carousel-image-download {
+ background-image: url(./images/carousel-sprite-2x.png?5);
+ }
+}
+
+/* reblog */
+div#carousel-reblog-box {
+ background: #222;
+ background: -moz-linear-gradient(bottom, #222, #333);
+ background: -webkit-gradient(linear, left bottom, left top, from(#222), to(#333));
+ padding: 3px 0 0;
+ display: none;
+ margin: 5px auto 0;
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+ border-radius: 2px;
+ -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.9);
+ -moz-box-shadow: 0 0 20px rgba(0,0,0,0.9);
+ box-shadow: 0 0 20px rgba(0,0,0,0.9);
+ height: 74px;
+ width: 565px;
+}
+
+#carousel-reblog-box textarea {
+ background: #999;
+ font: 13px/1.4 "Helvetica Neue", sans-serif !important;
+ color: #444;
+ padding: 3px 6px;
+ width: 370px;
+ height: 48px;
+ float: left;
+ margin: 6px 9px 0 9px;
+ border: 1px solid #666;
+ -webkit-box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+ border-radius: 2px;
+}
+
+#carousel-reblog-box textarea:focus {
+ background: #ccc;
+ color: #222;
+}
+
+#carousel-reblog-box label {
+ color: #aaa;
+ font-size: 11px;
+ padding-right: 2px;
+ padding-left: 2px;
+ display: inline;
+ font-weight: normal;
+}
+
+#carousel-reblog-box select {
+ width: 110px;
+ padding: 0;
+ font-size: 12px;
+ font-family: "Helvetica Neue", sans-serif !important;
+ background: #333;
+ color: #eee;
+ border: 1px solid #444;
+ margin-top:5px;
+}
+
+#carousel-reblog-box .submit,
+#wrapper #carousel-reblog-box p.response {
+ float: left;
+ width: 154px;
+ padding-top: 0;
+ padding-left: 1px;
+ overflow: hidden;
+ height: 34px;
+ margin:3px 0 0 2px !important;
+}
+
+#wrapper #carousel-reblog-box p.response {
+ font-size: 13px;
+ clear: none;
+ padding-left: 2px;
+ height: 34px;
+ color: #aaa;
+}
+
+#carousel-reblog-box input#carousel-reblog-submit, #jp-carousel-comment-form-button-submit {
+ font: 13px/24px "Helvetica Neue", sans-serif !important;
+ margin-top: 8px;
+ padding: 0 10px !important;
+ border-radius: 1em;
+ height: 24px;
+ color: #333;
+ cursor:pointer;
+ font-weight: normal;
+ background: #aaa;
+ background: -moz-linear-gradient(bottom, #aaa, #ccc);
+ background: -webkit-gradient(linear, left bottom, left top, from(#aaa), to(#ccc));
+ border: 1px solid #444;
+}
+
+#carousel-reblog-box input#carousel-reblog-submit:hover, #jp-carousel-comment-form-button-submit:hover {
+ background: #ccc;
+ background: -moz-linear-gradient(bottom, #ccc, #eee);
+ background: -webkit-gradient(linear, left bottom, left top, from(#ccc), to(#eee));
+}
+
+#carousel-reblog-box .canceltext {
+ color: #aaa;
+ font-size: 11px;
+ line-height: 24px;
+}
+
+#carousel-reblog-box .canceltext a {
+ color: #fff;
+}
+/* reblog end */
+
+
+/** Title and Desc Start **/
+.jp-carousel-titleanddesc {
+ border-top: 1px solid #222;
+ color: #999;
+ font-size: 15px;
+ padding-top: 24px;
+ margin-bottom: 20px;
+ font-weight:400;
+}
+.jp-carousel-titleanddesc-title {
+ font: 300 1.5em/1.1 "Helvetica Neue", sans-serif !important;
+ text-transform: none !important; /* prevents uppercase from leaking through */
+ color: #fff;
+ margin: 0 0 15px;
+ padding:0;
+}
+
+.jp-carousel-titleanddesc-desc p {
+ color: #999;
+ line-height:1.4;
+ margin-bottom: 0.75em;
+}
+
+.jp-carousel-titleanddesc p a,
+.jp-carousel-comments p a,
+.jp-carousel-info h2 a {
+ color: #fff !important;
+ border: none !important;
+ text-decoration: underline !important;
+ font-weight: normal !important;
+ font-style: normal !important;
+}
+
+.jp-carousel-titleanddesc p strong,
+.jp-carousel-titleanddesc p b {
+ font-weight: bold;
+ color: #999;
+}
+
+.jp-carousel-titleanddesc p em,
+.jp-carousel-titleanddesc p i {
+ font-style: italic;
+ color: #999;
+}
+
+
+.jp-carousel-titleanddesc p a:hover,
+.jp-carousel-comments p a:hover,
+.jp-carousel-info h2 a:hover {
+ color: #68c9e8 !important;
+}
+
+.jp-carousel-titleanddesc p:empty {
+ display: none;
+}
+
+.jp-carousel-photo-info h1:before,
+.jp-carousel-photo-info h1:after,
+.jp-carousel-left-column-wrapper h1:before,
+.jp-carousel-left-column-wrapper h1:after {
+ content:none !important;
+}
+/** Title and Desc End **/
+
+/** Meta Box Start **/
+.jp-carousel-image-meta {
+ background: #111;
+ border: 1px solid #222;
+ color: #fff;
+ font-size: 13px;
+ font: 12px/1.4 "Helvetica Neue", sans-serif !important;
+ overflow: hidden;
+ padding: 18px 20px;
+ width: 209px !important;
+}
+
+.jp-carousel-image-meta li,
+.jp-carousel-image-meta h5 {
+ font-family: "Helvetica Neue", sans-serif !important;
+ position: inherit !important;
+ top: auto !important;
+ right: auto !important;
+ left: auto !important;
+ bottom: auto !important;
+ background: none !important;
+ border: none !important;
+ font-weight: 400 !important;
+ line-height: 1.3em !important;
+}
+
+.jp-carousel-image-meta ul {
+ margin: 0 !important;
+ padding: 0 !important;
+ list-style: none !important;
+}
+
+.jp-carousel-image-meta li {
+ width: 48% !important;
+ display: inline-block !important;
+ vertical-align: top !important;
+ margin: 0 2% 15px 0 !important;
+ color: #fff !important;
+ font-size:13px !important;
+}
+
+.jp-carousel-image-meta h5 {
+ color: #999 !important;
+ text-transform: uppercase !important;
+ font-size:10px !important;
+ margin:0 0 2px !important;
+ letter-spacing: 0.1em !important;
+}
+
+a.jp-carousel-image-download {
+ padding-left: 23px;
+ display: inline-block;
+ clear: both;
+ color: #999;
+ line-height: 1;
+ font-weight: 400;
+ font-size: 13px;
+ text-decoration: none;
+ background-position: 0 -82px;
+}
+
+a.jp-carousel-image-download span.photo-size {
+ font-size: 11px;
+ border-radius: 1em;
+ margin-left: 2px;
+ display: inline-block;
+}
+
+a.jp-carousel-image-download span.photo-size-times {
+ padding: 0 1px 0 2px;
+}
+
+a.jp-carousel-image-download:hover {
+ background-position: 0 -122px;
+ color: #68c9e8;
+ border: none !important;
+}
+
+/** Meta Box End **/
+
+/** GPS Map Start **/
+.jp-carousel-image-map {
+ position: relative;
+ margin: -20px -20px 20px;
+ border-bottom: 1px solid rgba( 255, 255, 255, 0.17 );
+ height: 154px;
+}
+
+.jp-carousel-image-map img.gmap-main {
+ -moz-border-radius-topleft: 6px;
+ border-top-left-radius: 6px;
+ border-right: 1px solid rgba( 255, 255, 255, 0.17 );
+}
+.jp-carousel-image-map div.gmap-topright {
+ width: 94px;
+ height: 154px;
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+.jp-carousel-image-map div.imgclip {
+ overflow: hidden;
+ -moz-border-radius-topright: 6px;
+ border-top-right-radius: 6px;
+}
+.jp-carousel-image-map div.gmap-topright img {
+ margin-left: -40px;
+}
+.jp-carousel-image-map img.gmap-bottomright {
+ position: absolute;
+ top: 96px;
+ right: 0;
+}
+
+/** Comments Start **/
+.jp-carousel-comments {
+ font: 15px/1.7 "Helvetica Neue", sans-serif !important;
+ font-weight: 400;
+ background:none transparent;
+}
+
+.jp-carousel-comments p a:hover, .jp-carousel-comments p a:focus, .jp-carousel-comments p a:active {
+ color: #68c9e8 !important;
+}
+
+.jp-carousel-comment {
+ background:none transparent;
+ color: #999;
+ margin-bottom: 20px;
+ clear:left;
+ overflow: auto;
+ width: 100%
+}
+
+.jp-carousel-comment p {
+ color: #999 !important;
+}
+
+.jp-carousel-comment .comment-author {
+ font-size: 13px;
+ font-weight:400;
+ padding:0;
+ width:auto;
+ display: inline;
+ float:none;
+ border:none;
+ margin:0;
+}
+
+.jp-carousel-comment .comment-author a {
+ color: #fff;
+}
+
+.jp-carousel-comment .comment-gravatar {
+ float:left;
+}
+
+.jp-carousel-comment .comment-content {
+ border:none;
+ margin-left:85px;
+ padding: 0;
+}
+
+.jp-carousel-comment .avatar {
+ margin:0 20px 0 0;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ border: none !important;
+ padding: 0 !important;
+ background-color: transparent !important;
+}
+
+.jp-carousel-comment .comment-date {
+ color:#999;
+ margin-top: 4px;
+ font-size:11px;
+ display: inline;
+ float: right;
+ /*clear: right;*/
+}
+
+#jp-carousel-comment-form {
+ margin:0 0 10px !important;
+ float: left;
+ width: 100%;
+}
+
+textarea#jp-carousel-comment-form-comment-field {
+ background: rgba(34,34,34,0.9);
+ border: 1px solid #3a3a3a;
+ color: #aaa;
+ font: 15px/1.4 "Helvetica Neue", sans-serif !important;
+ width: 100%;
+ padding: 10px 10px 5px;
+ margin: 0;
+ float: none;
+ height: 147px;
+ -webkit-box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ overflow: hidden;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+textarea#jp-carousel-comment-form-comment-field::-webkit-input-placeholder {
+ color: #555;
+}
+
+textarea#jp-carousel-comment-form-comment-field:focus {
+ background: #ccc;
+ color: #222;
+}
+
+textarea#jp-carousel-comment-form-comment-field:focus::-webkit-input-placeholder {
+ color: #aaa;
+}
+
+#jp-carousel-comment-form-spinner {
+ color: #fff;
+ margin:22px 0 0 10px;
+ display: block;
+ width: 20px;
+ height: 20px;
+ float: left;
+}
+
+#jp-carousel-comment-form-submit-and-info-wrapper {
+ display: none;
+ /*margin-bottom:15px;*/
+ overflow: hidden;
+ width: 100%
+}
+
+#jp-carousel-comment-form-commenting-as {
+}
+
+#jp-carousel-comment-form-commenting-as input {
+ background: rgba(34,34,34,0.9);
+ border: 1px solid #3a3a3a;
+ color: #aaa;
+ font: 13px/1.4 "Helvetica Neue", sans-serif !important;
+ padding: 3px 6px;
+ float: left;
+ -webkit-box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+ border-radius: 2px;
+ width:285px;
+}
+
+#jp-carousel-comment-form-commenting-as input:focus {
+ background: #ccc;
+ color: #222;
+}
+
+#jp-carousel-comment-form-commenting-as p {
+ font: 400 13px/1.7 "Helvetica Neue", sans-serif !important;
+ margin:22px 0 0;
+ float: left;
+}
+
+#jp-carousel-comment-form-commenting-as fieldset {
+ float:left;
+ border:none;
+ margin:20px 0 0 0;
+ padding:0;
+}
+
+#jp-carousel-comment-form-commenting-as fieldset {
+ clear: both;
+}
+
+#jp-carousel-comment-form-commenting-as label {
+ font: 400 13px/1.7 "Helvetica Neue", sans-serif !important;
+ margin:0 20px 3px 0;
+ float:left;
+ width:100px;
+}
+
+#jp-carousel-comment-form-button-submit {
+ margin-top: 20px;
+ float:right;
+}
+
+#js-carousel-comment-form-container {
+ margin-bottom:15px;
+ overflow: auto;
+ width: 100%;
+}
+
+#jp-carousel-comment-form-container {
+ margin-bottom:15px;
+ overflow: auto;
+ width: 100%;
+}
+
+#jp-carousel-comment-post-results {
+ display: none;
+ overflow:auto;
+ width:100%;
+}
+
+#jp-carousel-comment-post-results span {
+ display:block;
+ text-align: center;
+ margin-top:20px;
+ width: 100%;
+ overflow: auto;
+ padding: 1em 0;
+ box-sizing: border-box;
+ background: rgba( 0, 0, 0, 0.7 );
+ border-radius: 2px;
+ font: 13px/1.4 "Helvetica Neue", sans-serif !important;
+ border: 1px solid rgba( 255, 255, 255, 0.17 );
+ -webkit-box-shadow: inset 0px 0px 5px 5px rgba(0, 0, 0, 1);
+ box-shadow: inset 0px 0px 5px 5px rgba(0, 0, 0, 1);
+}
+
+.jp-carousel-comment-post-error {
+ color:#DF4926;
+}
+
+.jp-carousel-comment-post-success {
+ /*color:#21759B;*/
+}
+
+#jp-carousel-comments-closed {
+ display: none;
+ color: #999;
+}
+
+#jp-carousel-comments-loading {
+ font: 400 15px/1.7 "Helvetica Neue", sans-serif !important;
+ display: none;
+ color: #999;
+ text-align: left;
+ margin-bottom: 20px;
+}
+
+
+/* ----- Light variant ----- */
+
+.jp-carousel-light .jp-carousel-overlay {
+ background: #fff;
+}
+
+.jp-carousel-light .jp-carousel-next-button:hover span,
+.jp-carousel-light .jp-carousel-previous-button:hover span {
+ opacity: 0.8;
+}
+
+.jp-carousel-light .jp-carousel-close-hint:hover,
+.jp-carousel-light .jp-carousel-titleanddesc div {
+ color: #000 !important;
+}
+
+.jp-carousel-light .jp-carousel-comments p a,
+.jp-carousel-light .jp-carousel-comment .comment-author a,
+.jp-carousel-light .jp-carousel-titleanddesc p a,
+.jp-carousel-light .jp-carousel-titleanddesc p a,
+.jp-carousel-light .jp-carousel-comments p a,
+.jp-carousel-light .jp-carousel-info h2 a {
+ color: #1e8cbe !important;
+}
+
+.jp-carousel-light .jp-carousel-comments p a:hover,
+.jp-carousel-light .jp-carousel-comment .comment-author a:hover,
+.jp-carousel-light .jp-carousel-titleanddesc p a:hover,
+.jp-carousel-light .jp-carousel-titleanddesc p a:hover,
+.jp-carousel-light .jp-carousel-comments p a:hover,
+.jp-carousel-light .jp-carousel-info h2 a:hover {
+ color: #f1831e !important;
+}
+
+.jp-carousel-light .jp-carousel-info h2,
+.jp-carousel-light .jp-carousel-titleanddesc,
+.jp-carousel-light .jp-carousel-titleanddesc p,
+.jp-carousel-light .jp-carousel-comment,
+.jp-carousel-light .jp-carousel-comment p,
+.jp-carousel-light div.jp-carousel-buttons a,
+.jp-carousel-light .jp-carousel-titleanddesc p strong,
+.jp-carousel-light .jp-carousel-titleanddesc p b,
+.jp-carousel-light .jp-carousel-titleanddesc p em,
+.jp-carousel-light .jp-carousel-titleanddesc p i {
+ color: #666;
+}
+
+.jp-carousel-light .jp-carousel-buttons {
+ border-bottom-color: #f0f0f0;
+ background: #f5f5f5;
+}
+
+.jp-carousel-light div.jp-carousel-buttons a:hover {
+ text-decoration: none;
+ color: #f1831e;
+}
+
+.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog,
+.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog:hover {
+ background-position: 4px -56px;
+ padding-left: 24px !important;
+}
+
+.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog.reblogged {
+ background-color: #2ea2cc;
+ color: #fff;
+}
+
+.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-commentlink {
+ background-position: 0px -176px;
+}
+
+.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog.reblogged {
+ background-position: 5px -36px;
+}
+
+.jp-carousel-light div#carousel-reblog-box {
+ background: #eee;
+ background: -moz-linear-gradient(bottom, #ececec, #f7f7f7);
+ background: -webkit-gradient(linear, left bottom, left top, from(#ececec), to(#f7f7f7));
+ -webkit-box-shadow: 0 2px 6px rgba(0,0,0,0.1);
+ -moz-box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ border:1px solid #ddd;
+}
+
+.jp-carousel-light #carousel-reblog-box textarea {
+ border: 1px inset #ccc;
+ color: #666;
+ border: 1px solid #cfcfcf;
+ background: #fff;
+}
+
+.jp-carousel-light #carousel-reblog-box .canceltext {
+ color: #888;
+}
+
+.jp-carousel-light #carousel-reblog-box .canceltext a {
+ color: #666;
+}
+
+.jp-carousel-light #carousel-reblog-box select {
+ background: #eee;
+ color: #333;
+ border: 1px solid #aaa;
+}
+
+.jp-carousel-light #carousel-reblog-box input#carousel-reblog-submit, #jp-carousel-comment-form-button-submit {
+ color: #333;
+ background: #fff;
+ background: -moz-linear-gradient(bottom, #ddd, #fff);
+ background: -webkit-gradient(linear, left bottom, left top, from(#ddd), to(#fff));
+ border: 1px solid #aaa;
+}
+
+.jp-carousel-light .jp-carousel-image-meta {
+ background: #fafafa;
+ border: 1px solid #eee;
+ border-top-color: #f5f5f5;
+ border-left-color: #f5f5f5;
+ color: #333;
+}
+
+.jp-carousel-light .jp-carousel-image-meta li {
+ color: #000 !important;
+}
+
+.jp-carousel-light .jp-carousel-close-hint {
+ color: #ccc;
+}
+
+.jp-carousel-light .jp-carousel-close-hint span {
+ background-color: white;
+ border-color: #ccc;
+}
+
+.jp-carousel-light #jp-carousel-comment-form-comment-field::-webkit-input-placeholder {
+ color: #aaa;
+}
+
+.jp-carousel-light #jp-carousel-comment-form-comment-field:focus {
+ color: #333;
+}
+
+.jp-carousel-light #jp-carousel-comment-form-comment-field:focus::-webkit-input-placeholder {
+ color: #ddd;
+}
+
+.jp-carousel-light a.jp-carousel-image-download {
+ background-position: 0 -122px;
+}
+
+.jp-carousel-light a.jp-carousel-image-download:hover {
+ background-position: 0 -122px;
+ color: #f1831e;
+}
+
+.jp-carousel-light textarea#jp-carousel-comment-form-comment-field {
+ background: #fbfbfb;
+ color: #333;
+ border: 1px solid #dfdfdf;
+ -webkit-box-shadow: inset 2px 2px 2px rgba(0,0,0,0.1);
+ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.1);
+}
+
+.jp-carousel-light #jp-carousel-comment-form-commenting-as input {
+ background: #fbfbfb;
+ border: 1px solid #dfdfdf;
+ color: #333;
+ -webkit-box-shadow: inset 2px 2px 2px rgba(0,0,0,0.1);
+ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.1);
+}
+
+.jp-carousel-light #jp-carousel-comment-form-commenting-as input:focus {
+ background: #fbfbfb;
+ color: #333;
+}
+
+.jp-carousel-light #jp-carousel-comment-post-results span {
+ background: #f7f7f7;
+ border:1px solid #dfdfdf;
+ -webkit-box-shadow: inset 0px 0px 5px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0px 0px 5px rgba(0, 0, 0, 0.05);
+}
+
+.jp-carousel-light .jp-carousel-slide {
+ background-color:#fff;
+}
+
+.jp-carousel-light .jp-carousel-titleanddesc {
+ border-top: 1px solid #eee;
+}
+
+.jp-carousel-light .jp-carousel-fadeaway {
+ background: -moz-linear-gradient(bottom, rgba(255,255,255,0.75), rgba(255,255,255,0));
+ background: -webkit-gradient(linear, left bottom, left top, from(rgba(255,255,255,0.75)), to(rgba(255,255,255,0)));
+}
+
+/* Small screens */
+@media only screen and (max-width: 760px) {
+
+ .jp-carousel-info {
+ margin: 0 10px !important;
+ }
+
+ .jp-carousel-next-button, .jp-carousel-previous-button {
+ display: none !important;
+ }
+
+ .jp-carousel-buttons {
+ display: none !important;
+ }
+
+ .jp-carousel-image-meta {
+ float: none !important;
+ width: 100% !important;
+ -moz-box-sizing:border-box;
+ -webkit-box-sizing:border-box;
+ box-sizing: border-box;
+ }
+
+ .jp-carousel-close-hint {
+ font-weight: 800 !important;
+ font-size: 26px !important;
+ position: fixed !important;
+ top: -10px;
+ }
+
+ .jp-carousel-slide img {
+ filter: alpha(opacity=100);
+ opacity: 1;
+ }
+
+ .jp-carousel-wrap {
+ background-color: #000;
+ }
+
+ .jp-carousel-fadeaway {
+ display: none;
+ }
+
+ #jp-carousel-comment-form-container {
+ display: none !important;
+ }
+
+ .jp-carousel-titleanddesc {
+ padding-top: 0 !important;
+ border: none !important;
+ }
+ .jp-carousel-titleanddesc-title {
+ font-size: 1em !important;
+ }
+
+ .jp-carousel-left-column-wrapper {
+ padding: 0;
+ width: 100% !important;
+ }
+
+ .jp-carousel-photo-info {
+ left: 0 !important;
+ width: 100% !important;
+ }
+}
diff --git a/plugins/jetpack/modules/carousel/jetpack-carousel.js b/plugins/jetpack/modules/carousel/jetpack-carousel.js
new file mode 100644
index 00000000..3c7a5b45
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/jetpack-carousel.js
@@ -0,0 +1,1851 @@
+/* jshint sub: true, onevar: false, multistr: true, devel: true, smarttabs: true */
+/* global jetpackCarouselStrings, DocumentTouch */
+
+jQuery( document ).ready( function( $ ) {
+ // gallery faded layer and container elements
+ var overlay,
+ comments,
+ gallery,
+ container,
+ nextButton,
+ previousButton,
+ info,
+ transitionBegin,
+ caption,
+ resizeTimeout,
+ photo_info,
+ close_hint,
+ commentInterval,
+ lastSelectedSlide,
+ screenPadding = 110,
+ originalOverflow = $( 'body' ).css( 'overflow' ),
+ originalHOverflow = $( 'html' ).css( 'overflow' ),
+ proportion = 85,
+ last_known_location_hash = '',
+ imageMeta,
+ titleAndDescription,
+ commentForm,
+ leftColWrapper,
+ scrollPos;
+
+ if ( window.innerWidth <= 760 ) {
+ screenPadding = Math.round( ( window.innerWidth / 760 ) * 110 );
+
+ if (
+ screenPadding < 40 &&
+ ( 'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch ) )
+ ) {
+ screenPadding = 0;
+ }
+ }
+
+ // Adding a polyfill for browsers that do not have Date.now
+ if ( 'undefined' === typeof Date.now ) {
+ Date.now = function now() {
+ return new Date().getTime();
+ };
+ }
+
+ var keyListener = function( e ) {
+ switch ( e.which ) {
+ case 38: // up
+ e.preventDefault();
+ container.scrollTop( container.scrollTop() - 100 );
+ break;
+ case 40: // down
+ e.preventDefault();
+ container.scrollTop( container.scrollTop() + 100 );
+ break;
+ case 39: // right
+ e.preventDefault();
+ gallery.jp_carousel( 'next' );
+ break;
+ case 37: // left
+ case 8: // backspace
+ e.preventDefault();
+ gallery.jp_carousel( 'previous' );
+ break;
+ case 27: // escape
+ e.preventDefault();
+ container.jp_carousel( 'close' );
+ break;
+ default:
+ // making jslint happy
+ break;
+ }
+ };
+
+ var resizeListener = function(/*e*/) {
+ clearTimeout( resizeTimeout );
+ resizeTimeout = setTimeout( function() {
+ gallery.jp_carousel( 'slides' ).jp_carousel( 'fitSlide', true );
+ gallery.jp_carousel( 'updateSlidePositions', true );
+ gallery.jp_carousel( 'fitMeta', true );
+ }, 200 );
+ };
+
+ var prepareGallery = function(/*dataCarouselExtra*/) {
+ if ( ! overlay ) {
+ overlay = $( '<div></div>' )
+ .addClass( 'jp-carousel-overlay' )
+ .css( {
+ position: 'fixed',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ } );
+
+ var buttons =
+ '<a class="jp-carousel-commentlink" href="#">' + jetpackCarouselStrings.comment + '</a>';
+ if ( 1 === Number( jetpackCarouselStrings.is_logged_in ) ) {
+ }
+
+ buttons = $( '<div class="jp-carousel-buttons">' + buttons + '</div>' );
+
+ caption = $( '<h2 itemprop="caption description"></h2>' );
+ photo_info = $( '<div class="jp-carousel-photo-info"></div>' ).append( caption );
+
+ imageMeta = $( '<div></div>' )
+ .addClass( 'jp-carousel-image-meta' )
+ .css( {
+ float: 'right',
+ 'margin-top': '20px',
+ width: '250px',
+ } );
+
+ imageMeta
+ .append( buttons )
+ .append( "<ul class='jp-carousel-image-exif' style='display:none;'></ul>" )
+ .append( "<a class='jp-carousel-image-download' style='display:none;'></a>" )
+ .append( "<div class='jp-carousel-image-map' style='display:none;'></div>" );
+
+ titleAndDescription = $( '<div></div>' )
+ .addClass( 'jp-carousel-titleanddesc' )
+ .css( {
+ width: '100%',
+ 'margin-top': imageMeta.css( 'margin-top' ),
+ } );
+
+ var commentFormMarkup = '<div id="jp-carousel-comment-form-container">';
+
+ if (
+ jetpackCarouselStrings.local_comments_commenting_as &&
+ jetpackCarouselStrings.local_comments_commenting_as.length
+ ) {
+ // Comments not enabled, fallback to local comments
+
+ if (
+ 1 !== Number( jetpackCarouselStrings.is_logged_in ) &&
+ 1 === Number( jetpackCarouselStrings.comment_registration )
+ ) {
+ commentFormMarkup +=
+ '<div id="jp-carousel-comment-form-commenting-as">' +
+ jetpackCarouselStrings.local_comments_commenting_as +
+ '</div>';
+ } else {
+ commentFormMarkup += '<form id="jp-carousel-comment-form">';
+ commentFormMarkup +=
+ '<textarea name="comment" class="jp-carousel-comment-form-field jp-carousel-comment-form-textarea" id="jp-carousel-comment-form-comment-field" placeholder="' +
+ jetpackCarouselStrings.write_comment +
+ '"></textarea>';
+ commentFormMarkup += '<div id="jp-carousel-comment-form-submit-and-info-wrapper">';
+ commentFormMarkup +=
+ '<div id="jp-carousel-comment-form-commenting-as">' +
+ jetpackCarouselStrings.local_comments_commenting_as +
+ '</div>';
+ commentFormMarkup +=
+ '<input type="submit" name="submit" class="jp-carousel-comment-form-button" id="jp-carousel-comment-form-button-submit" value="' +
+ jetpackCarouselStrings.post_comment +
+ '" />';
+ commentFormMarkup += '<span id="jp-carousel-comment-form-spinner">&nbsp;</span>';
+ commentFormMarkup += '<div id="jp-carousel-comment-post-results"></div>';
+ commentFormMarkup += '</div>';
+ commentFormMarkup += '</form>';
+ }
+ }
+ commentFormMarkup += '</div>';
+
+ commentForm = $( commentFormMarkup ).css( {
+ width: '100%',
+ 'margin-top': '20px',
+ color: '#999',
+ } );
+
+ comments = $( '<div></div>' )
+ .addClass( 'jp-carousel-comments' )
+ .css( {
+ width: '100%',
+ bottom: '10px',
+ 'margin-top': '20px',
+ } );
+
+ var commentsLoading = $(
+ '<div id="jp-carousel-comments-loading"><span>' +
+ jetpackCarouselStrings.loading_comments +
+ '</span></div>'
+ ).css( {
+ width: '100%',
+ bottom: '10px',
+ 'margin-top': '20px',
+ } );
+
+ var leftWidth = $( window ).width() - screenPadding * 2 - ( imageMeta.width() + 40 );
+ leftWidth += 'px';
+
+ leftColWrapper = $( '<div></div>' )
+ .addClass( 'jp-carousel-left-column-wrapper' )
+ .css( {
+ width: Math.floor( leftWidth ),
+ } )
+ .append( titleAndDescription )
+ .append( commentForm )
+ .append( comments )
+ .append( commentsLoading );
+
+ var fadeaway = $( '<div></div>' ).addClass( 'jp-carousel-fadeaway' );
+
+ info = $( '<div></div>' )
+ .addClass( 'jp-carousel-info' )
+ .css( {
+ top: Math.floor( ( $( window ).height() / 100 ) * proportion ),
+ left: screenPadding,
+ right: screenPadding,
+ } )
+ .append( photo_info )
+ .append( imageMeta );
+
+ if ( window.innerWidth <= 760 ) {
+ photo_info.remove().insertAfter( titleAndDescription );
+ info.prepend( leftColWrapper );
+ } else {
+ info.append( leftColWrapper );
+ }
+
+ var targetBottomPos = $( window ).height() - parseInt( info.css( 'top' ), 10 ) + 'px';
+
+ nextButton = $( '<div><span></span></div>' )
+ .addClass( 'jp-carousel-next-button' )
+ .css( {
+ right: '15px',
+ } )
+ .hide();
+
+ previousButton = $( '<div><span></span></div>' )
+ .addClass( 'jp-carousel-previous-button' )
+ .css( {
+ left: 0,
+ } )
+ .hide();
+
+ nextButton.add( previousButton ).css( {
+ position: 'fixed',
+ top: '40px',
+ bottom: targetBottomPos,
+ width: screenPadding,
+ } );
+
+ gallery = $( '<div></div>' )
+ .addClass( 'jp-carousel' )
+ .css( {
+ position: 'absolute',
+ top: 0,
+ bottom: targetBottomPos,
+ left: 0,
+ right: 0,
+ } );
+
+ close_hint = $( '<div class="jp-carousel-close-hint"><span>&times;</span></div>' ).css( {
+ position: 'fixed',
+ } );
+
+ container = $( '<div></div>' )
+ .addClass( 'jp-carousel-wrap' )
+ .addClass( 'jp-carousel-transitions' );
+ if ( 'white' === jetpackCarouselStrings.background_color ) {
+ container.addClass( 'jp-carousel-light' );
+ }
+
+ container.attr( 'itemscope', '' );
+
+ container.attr( 'itemtype', 'https://schema.org/ImageGallery' );
+
+ container
+ .css( {
+ position: 'fixed',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ 'z-index': 2147483647,
+ 'overflow-x': 'hidden',
+ 'overflow-y': 'auto',
+ direction: 'ltr',
+ } )
+ .hide()
+ .append( overlay )
+ .append( gallery )
+ .append( fadeaway )
+ .append( info )
+ .append( nextButton )
+ .append( previousButton )
+ .append( close_hint )
+ .appendTo( $( 'body' ) )
+ .click( function( e ) {
+ var target = $( e.target ),
+ wrap = target.parents( 'div.jp-carousel-wrap' ),
+ data = wrap.data( 'carousel-extra' ),
+ slide = wrap.find( 'div.selected' ),
+ attachment_id = slide.data( 'attachment-id' );
+ data = data || [];
+
+ if (
+ target.is( gallery ) ||
+ target
+ .parents()
+ .add( target )
+ .is( close_hint )
+ ) {
+ container.jp_carousel( 'close' );
+ } else if ( target.hasClass( 'jp-carousel-commentlink' ) ) {
+ e.preventDefault();
+ e.stopPropagation();
+ $( window ).unbind( 'keydown', keyListener );
+ container.animate( { scrollTop: parseInt( info.position()[ 'top' ], 10 ) }, 'fast' );
+ $( '#jp-carousel-comment-form-submit-and-info-wrapper' ).slideDown( 'fast' );
+ $( '#jp-carousel-comment-form-comment-field' ).focus();
+ } else if ( target.hasClass( 'jp-carousel-comment-login' ) ) {
+ var url = jetpackCarouselStrings.login_url + '%23jp-carousel-' + attachment_id;
+
+ window.location.href = url;
+ } else if ( target.parents( '#jp-carousel-comment-form-container' ).length ) {
+ var textarea = $( '#jp-carousel-comment-form-comment-field' )
+ .blur( function() {
+ $( window ).bind( 'keydown', keyListener );
+ } )
+ .focus( function() {
+ $( window ).unbind( 'keydown', keyListener );
+ } );
+
+ var emailField = $( '#jp-carousel-comment-form-email-field' )
+ .blur( function() {
+ $( window ).bind( 'keydown', keyListener );
+ } )
+ .focus( function() {
+ $( window ).unbind( 'keydown', keyListener );
+ } );
+
+ var authorField = $( '#jp-carousel-comment-form-author-field' )
+ .blur( function() {
+ $( window ).bind( 'keydown', keyListener );
+ } )
+ .focus( function() {
+ $( window ).unbind( 'keydown', keyListener );
+ } );
+
+ var urlField = $( '#jp-carousel-comment-form-url-field' )
+ .blur( function() {
+ $( window ).bind( 'keydown', keyListener );
+ } )
+ .focus( function() {
+ $( window ).unbind( 'keydown', keyListener );
+ } );
+
+ if ( textarea && textarea.attr( 'id' ) === target.attr( 'id' ) ) {
+ // For first page load
+ $( window ).unbind( 'keydown', keyListener );
+ $( '#jp-carousel-comment-form-submit-and-info-wrapper' ).slideDown( 'fast' );
+ } else if ( target.is( 'input[type="submit"]' ) ) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ $( '#jp-carousel-comment-form-spinner' ).spin( 'small', 'white' );
+
+ var ajaxData = {
+ action: 'post_attachment_comment',
+ nonce: jetpackCarouselStrings.nonce,
+ blog_id: data[ 'blog_id' ],
+ id: attachment_id,
+ comment: textarea.val(),
+ };
+
+ if ( ! ajaxData[ 'comment' ].length ) {
+ gallery.jp_carousel( 'postCommentError', {
+ field: 'jp-carousel-comment-form-comment-field',
+ error: jetpackCarouselStrings.no_comment_text,
+ } );
+ return;
+ }
+
+ if ( 1 !== Number( jetpackCarouselStrings.is_logged_in ) ) {
+ ajaxData[ 'email' ] = emailField.val();
+ ajaxData[ 'author' ] = authorField.val();
+ ajaxData[ 'url' ] = urlField.val();
+
+ if ( 1 === Number( jetpackCarouselStrings.require_name_email ) ) {
+ if ( ! ajaxData[ 'email' ].length || ! ajaxData[ 'email' ].match( '@' ) ) {
+ gallery.jp_carousel( 'postCommentError', {
+ field: 'jp-carousel-comment-form-email-field',
+ error: jetpackCarouselStrings.no_comment_email,
+ } );
+ return;
+ } else if ( ! ajaxData[ 'author' ].length ) {
+ gallery.jp_carousel( 'postCommentError', {
+ field: 'jp-carousel-comment-form-author-field',
+ error: jetpackCarouselStrings.no_comment_author,
+ } );
+ return;
+ }
+ }
+ }
+
+ $.ajax( {
+ type: 'POST',
+ url: jetpackCarouselStrings.ajaxurl,
+ data: ajaxData,
+ dataType: 'json',
+ success: function( response /*, status, xhr*/ ) {
+ if ( 'approved' === response.comment_status ) {
+ $( '#jp-carousel-comment-post-results' )
+ .slideUp( 'fast' )
+ .html(
+ '<span class="jp-carousel-comment-post-success">' +
+ jetpackCarouselStrings.comment_approved +
+ '</span>'
+ )
+ .slideDown( 'fast' );
+ } else if ( 'unapproved' === response.comment_status ) {
+ $( '#jp-carousel-comment-post-results' )
+ .slideUp( 'fast' )
+ .html(
+ '<span class="jp-carousel-comment-post-success">' +
+ jetpackCarouselStrings.comment_unapproved +
+ '</span>'
+ )
+ .slideDown( 'fast' );
+ } else {
+ // 'deleted', 'spam', false
+ $( '#jp-carousel-comment-post-results' )
+ .slideUp( 'fast' )
+ .html(
+ '<span class="jp-carousel-comment-post-error">' +
+ jetpackCarouselStrings.comment_post_error +
+ '</span>'
+ )
+ .slideDown( 'fast' );
+ }
+ gallery.jp_carousel( 'clearCommentTextAreaValue' );
+ gallery.jp_carousel( 'getComments', {
+ attachment_id: attachment_id,
+ offset: 0,
+ clear: true,
+ } );
+ $( '#jp-carousel-comment-form-button-submit' ).val(
+ jetpackCarouselStrings.post_comment
+ );
+ $( '#jp-carousel-comment-form-spinner' ).spin( false );
+ },
+ error: function(/*xhr, status, error*/) {
+ // TODO: Add error handling and display here
+ gallery.jp_carousel( 'postCommentError', {
+ field: 'jp-carousel-comment-form-comment-field',
+ error: jetpackCarouselStrings.comment_post_error,
+ } );
+ return;
+ },
+ } );
+ }
+ } else if ( ! target.parents( '.jp-carousel-info' ).length ) {
+ container.jp_carousel( 'next' );
+ }
+ } )
+ .bind( 'jp_carousel.afterOpen', function() {
+ $( window ).bind( 'keydown', keyListener );
+ $( window ).bind( 'resize', resizeListener );
+ gallery.opened = true;
+
+ resizeListener();
+ } )
+ .bind( 'jp_carousel.beforeClose', function() {
+ var scroll = $( window ).scrollTop();
+
+ $( window ).unbind( 'keydown', keyListener );
+ $( window ).unbind( 'resize', resizeListener );
+ $( window ).scrollTop( scroll );
+ $( '.jp-carousel-previous-button' ).hide();
+ $( '.jp-carousel-next-button' ).hide();
+ } )
+ .bind( 'jp_carousel.afterClose', function() {
+ if ( window.location.hash && history.back ) {
+ history.back();
+ }
+ last_known_location_hash = '';
+ gallery.opened = false;
+ } )
+ .on( 'transitionend.jp-carousel ', '.jp-carousel-slide', function( e ) {
+ // If the movement transitions take more than twice the allotted time, disable them.
+ // There is some wiggle room in the 2x, since some of that time is taken up in
+ // JavaScript, setting up the transition and calling the events.
+ if ( 'transform' === e.originalEvent.propertyName ) {
+ var transitionMultiplier =
+ ( Date.now() - transitionBegin ) / 1000 / e.originalEvent.elapsedTime;
+
+ container.off( 'transitionend.jp-carousel' );
+
+ if ( transitionMultiplier >= 2 ) {
+ $( '.jp-carousel-transitions' ).removeClass( 'jp-carousel-transitions' );
+ }
+ }
+ } );
+
+ $( '.jp-carousel-wrap' ).touchwipe( {
+ wipeLeft: function( e ) {
+ e.preventDefault();
+ gallery.jp_carousel( 'next' );
+ },
+ wipeRight: function( e ) {
+ e.preventDefault();
+ gallery.jp_carousel( 'previous' );
+ },
+ preventDefaultEvents: false,
+ } );
+
+ nextButton.add( previousButton ).click( function( e ) {
+ e.preventDefault();
+ e.stopPropagation();
+ if ( nextButton.is( this ) ) {
+ gallery.jp_carousel( 'next' );
+ } else {
+ gallery.jp_carousel( 'previous' );
+ }
+ } );
+ }
+ };
+
+ var processSingleImageGallery = function() {
+ // process links that contain img tag with attribute data-attachment-id
+ $( 'a img[data-attachment-id]' ).each( function() {
+ var container = $( this ).parent();
+
+ // skip if image was already added to gallery by shortcode
+ if ( container.parent( '.gallery-icon' ).length ) {
+ return;
+ }
+
+ // skip if the container is not a link
+ if ( 'undefined' === typeof $( container ).attr( 'href' ) ) {
+ return;
+ }
+
+ var valid = false;
+
+ // if link points to 'Media File' (ignoring GET parameters) and flag is set allow it
+ if (
+ $( container )
+ .attr( 'href' )
+ .split( '?' )[ 0 ] ===
+ $( this )
+ .attr( 'data-orig-file' )
+ .split( '?' )[ 0 ] &&
+ 1 === Number( jetpackCarouselStrings.single_image_gallery_media_file )
+ ) {
+ valid = true;
+ }
+
+ // if link points to 'Attachment Page' allow it
+ if ( $( container ).attr( 'href' ) === $( this ).attr( 'data-permalink' ) ) {
+ valid = true;
+ }
+
+ // links to 'Custom URL' or 'Media File' when flag not set are not valid
+ if ( ! valid ) {
+ return;
+ }
+
+ // make this node a gallery recognizable by event listener above
+ $( container ).addClass( 'single-image-gallery' );
+ // blog_id is needed to allow posting comments to correct blog
+ $( container ).data( 'carousel-extra', {
+ blog_id: Number( jetpackCarouselStrings.blog_id ),
+ } );
+ } );
+ };
+
+ var methods = {
+ testForData: function( gallery ) {
+ gallery = $( gallery ); // make sure we have it as a jQuery object.
+ return ! ( ! gallery.length || ! gallery.data( 'carousel-extra' ) );
+ },
+
+ testIfOpened: function() {
+ return !! (
+ 'undefined' !== typeof gallery &&
+ 'undefined' !== typeof gallery.opened &&
+ gallery.opened
+ );
+ },
+
+ openOrSelectSlide: function( index ) {
+ // The `open` method triggers an asynchronous effect, so we will get an
+ // error if we try to use `open` then `selectSlideAtIndex` immediately
+ // after it. We can only use `selectSlideAtIndex` if the carousel is
+ // already open.
+ if ( ! $( this ).jp_carousel( 'testIfOpened' ) ) {
+ // The `open` method selects the correct slide during the
+ // initialization.
+ $( this ).jp_carousel( 'open', { start_index: index } );
+ } else {
+ gallery.jp_carousel( 'selectSlideAtIndex', index );
+ }
+ },
+
+ open: function( options ) {
+ var settings = {
+ items_selector:
+ '.gallery-item [data-attachment-id], .tiled-gallery-item [data-attachment-id], img[data-attachment-id]',
+ start_index: 0,
+ },
+ data = $( this ).data( 'carousel-extra' );
+
+ if ( ! data ) {
+ return; // don't run if the default gallery functions weren't used
+ }
+
+ prepareGallery( data );
+
+ if ( gallery.jp_carousel( 'testIfOpened' ) ) {
+ return; // don't open if already opened
+ }
+
+ // make sure to stop the page from scrolling behind the carousel overlay, so we don't trigger
+ // infiniscroll for it when enabled (Reader, theme infiniscroll, etc).
+ originalOverflow = $( 'body' ).css( 'overflow' );
+ $( 'body' ).css( 'overflow', 'hidden' );
+ // prevent html from overflowing on some of the new themes.
+ originalHOverflow = $( 'html' ).css( 'overflow' );
+ $( 'html' ).css( 'overflow', 'hidden' );
+ scrollPos = $( window ).scrollTop();
+
+ container.data( 'carousel-extra', data );
+
+ return this.each( function() {
+ // If options exist, lets merge them
+ // with our default settings
+ var $this = $( this );
+
+ if ( options ) {
+ $.extend( settings, options );
+ }
+ if ( -1 === settings.start_index ) {
+ settings.start_index = 0; //-1 returned if can't find index, so start from beginning
+ }
+
+ container.trigger( 'jp_carousel.beforeOpen' ).fadeIn( 'fast', function() {
+ container.trigger( 'jp_carousel.afterOpen' );
+ gallery
+ .jp_carousel(
+ 'initSlides',
+ $this.find( settings.items_selector ),
+ settings.start_index
+ )
+ .jp_carousel( 'selectSlideAtIndex', settings.start_index );
+ } );
+ gallery.html( '' );
+ } );
+ },
+
+ selectSlideAtIndex: function( index ) {
+ var slides = this.jp_carousel( 'slides' ),
+ selected = slides.eq( index );
+
+ if ( 0 === selected.length ) {
+ selected = slides.eq( 0 );
+ }
+
+ gallery.jp_carousel( 'selectSlide', selected, false );
+ return this;
+ },
+
+ close: function() {
+ // make sure to let the page scroll again
+ $( 'body' ).css( 'overflow', originalOverflow );
+ $( 'html' ).css( 'overflow', originalHOverflow );
+ this.jp_carousel( 'clearCommentTextAreaValue' );
+ return container.trigger( 'jp_carousel.beforeClose' ).fadeOut( 'fast', function() {
+ container.trigger( 'jp_carousel.afterClose' );
+ $( window ).scrollTop( scrollPos );
+ } );
+ },
+
+ next: function() {
+ this.jp_carousel( 'previousOrNext', 'nextSlide' );
+ },
+
+ previous: function() {
+ this.jp_carousel( 'previousOrNext', 'prevSlide' );
+ },
+
+ previousOrNext: function( slideSelectionMethodName ) {
+ if ( ! this.jp_carousel( 'hasMultipleImages' ) ) {
+ return false;
+ }
+
+ var slide = gallery.jp_carousel( slideSelectionMethodName );
+
+ if ( slide ) {
+ container.animate( { scrollTop: 0 }, 'fast' );
+ this.jp_carousel( 'clearCommentTextAreaValue' );
+ this.jp_carousel( 'selectSlide', slide );
+ }
+ },
+
+ selectedSlide: function() {
+ return this.find( '.selected' );
+ },
+
+ setSlidePosition: function( x ) {
+ transitionBegin = Date.now();
+
+ return this.css( {
+ '-webkit-transform': 'translate3d(' + x + 'px,0,0)',
+ '-moz-transform': 'translate3d(' + x + 'px,0,0)',
+ '-ms-transform': 'translate(' + x + 'px,0)',
+ '-o-transform': 'translate(' + x + 'px,0)',
+ transform: 'translate3d(' + x + 'px,0,0)',
+ } );
+ },
+
+ updateSlidePositions: function( animate ) {
+ var current = this.jp_carousel( 'selectedSlide' ),
+ galleryWidth = gallery.width(),
+ currentWidth = current.width(),
+ previous = gallery.jp_carousel( 'prevSlide' ),
+ next = gallery.jp_carousel( 'nextSlide' ),
+ previousPrevious = previous.prev(),
+ nextNext = next.next(),
+ left = Math.floor( ( galleryWidth - currentWidth ) * 0.5 );
+
+ current.jp_carousel( 'setSlidePosition', left ).show();
+
+ // minimum width
+ gallery.jp_carousel( 'fitInfo', animate );
+
+ // prep the slides
+ var direction = lastSelectedSlide.is( current.prevAll() ) ? 1 : -1;
+
+ // Since we preload the `previousPrevious` and `nextNext` slides, we need
+ // to make sure they technically visible in the DOM, but invisible to the
+ // user. To hide them from the user, we position them outside the edges
+ // of the window.
+ //
+ // This section of code only applies when there are more than three
+ // slides. Otherwise, the `previousPrevious` and `nextNext` slides will
+ // overlap with the `previous` and `next` slides which must be visible
+ // regardless.
+ if ( 1 === direction ) {
+ if ( ! nextNext.is( previous ) ) {
+ nextNext.jp_carousel( 'setSlidePosition', galleryWidth + next.width() ).show();
+ }
+
+ if ( ! previousPrevious.is( next ) ) {
+ previousPrevious
+ .jp_carousel( 'setSlidePosition', -previousPrevious.width() - currentWidth )
+ .show();
+ }
+ } else {
+ if ( ! nextNext.is( previous ) ) {
+ nextNext.jp_carousel( 'setSlidePosition', galleryWidth + currentWidth ).show();
+ }
+ }
+
+ previous
+ .jp_carousel( 'setSlidePosition', Math.floor( -previous.width() + screenPadding * 0.75 ) )
+ .show();
+ next
+ .jp_carousel( 'setSlidePosition', Math.ceil( galleryWidth - screenPadding * 0.75 ) )
+ .show();
+ },
+
+ selectSlide: function( slide, animate ) {
+ lastSelectedSlide = this.find( '.selected' ).removeClass( 'selected' );
+
+ var slides = gallery.jp_carousel( 'slides' ).css( { position: 'fixed' } ),
+ current = $( slide )
+ .addClass( 'selected' )
+ .css( { position: 'relative' } ),
+ attachmentId = current.data( 'attachment-id' ),
+ previous = gallery.jp_carousel( 'prevSlide' ),
+ next = gallery.jp_carousel( 'nextSlide' ),
+ previousPrevious = previous.prev(),
+ nextNext = next.next(),
+ animated,
+ captionHtml;
+
+ // center the main image
+ gallery.jp_carousel( 'loadFullImage', current );
+
+ caption.hide();
+
+ if ( next.length === 0 && slides.length <= 2 ) {
+ $( '.jp-carousel-next-button' ).hide();
+ } else {
+ $( '.jp-carousel-next-button' ).show();
+ }
+
+ if ( previous.length === 0 && slides.length <= 2 ) {
+ $( '.jp-carousel-previous-button' ).hide();
+ } else {
+ $( '.jp-carousel-previous-button' ).show();
+ }
+
+ animated = current
+ .add( previous )
+ .add( previousPrevious )
+ .add( next )
+ .add( nextNext )
+ .jp_carousel( 'loadSlide' );
+
+ // slide the whole view to the x we want
+ slides.not( animated ).hide();
+
+ gallery.jp_carousel( 'updateSlidePositions', animate );
+
+ container.trigger( 'jp_carousel.selectSlide', [ current ] );
+
+ gallery.jp_carousel( 'getTitleDesc', {
+ title: current.data( 'title' ),
+ desc: current.data( 'desc' ),
+ } );
+
+ var imageMeta = current.data( 'image-meta' );
+ gallery.jp_carousel( 'updateExif', imageMeta );
+ gallery.jp_carousel( 'updateFullSizeLink', current );
+ gallery.jp_carousel( 'updateMap', imageMeta );
+ gallery.jp_carousel( 'testCommentsOpened', current.data( 'comments-opened' ) );
+ gallery.jp_carousel( 'getComments', {
+ attachment_id: attachmentId,
+ offset: 0,
+ clear: true,
+ } );
+ $( '#jp-carousel-comment-post-results' ).slideUp();
+
+ // $('<div />').text(sometext).html() is a trick to go to HTML to plain
+ // text (including HTML entities decode, etc)
+ if ( current.data( 'caption' ) ) {
+ captionHtml = $( '<div />' )
+ .text( current.data( 'caption' ) )
+ .html();
+
+ if (
+ captionHtml ===
+ $( '<div />' )
+ .text( current.data( 'title' ) )
+ .html()
+ ) {
+ $( '.jp-carousel-titleanddesc-title' )
+ .fadeOut( 'fast' )
+ .empty();
+ }
+
+ if (
+ captionHtml ===
+ $( '<div />' )
+ .text( current.data( 'desc' ) )
+ .html()
+ ) {
+ $( '.jp-carousel-titleanddesc-desc' )
+ .fadeOut( 'fast' )
+ .empty();
+ }
+
+ caption.html( current.data( 'caption' ) ).fadeIn( 'slow' );
+ } else {
+ caption.fadeOut( 'fast' ).empty();
+ }
+
+ // Record pageview in WP Stats, for each new image loaded full-screen.
+ if ( jetpackCarouselStrings.stats ) {
+ new Image().src =
+ document.location.protocol +
+ '//pixel.wp.com/g.gif?' +
+ jetpackCarouselStrings.stats +
+ '&post=' +
+ encodeURIComponent( attachmentId ) +
+ '&rand=' +
+ Math.random();
+ }
+
+ // Load the images for the next and previous slides.
+ $( next )
+ .add( previous )
+ .each( function() {
+ gallery.jp_carousel( 'loadFullImage', $( this ) );
+ } );
+
+ window.location.hash = last_known_location_hash = '#jp-carousel-' + attachmentId;
+ },
+
+ slides: function() {
+ return this.find( '.jp-carousel-slide' );
+ },
+
+ slideDimensions: function() {
+ return {
+ width: $( window ).width() - screenPadding * 2,
+ height: Math.floor( ( $( window ).height() / 100 ) * proportion - 60 ),
+ };
+ },
+
+ loadSlide: function() {
+ return this.each( function() {
+ var slide = $( this );
+ slide.find( 'img' ).one( 'load', function() {
+ // set the width/height of the image if it's too big
+ slide.jp_carousel( 'fitSlide', false );
+ } );
+ } );
+ },
+
+ bestFit: function() {
+ var max = gallery.jp_carousel( 'slideDimensions' ),
+ orig = this.jp_carousel( 'originalDimensions' ),
+ orig_ratio = orig.width / orig.height,
+ w_ratio = 1,
+ h_ratio = 1,
+ width,
+ height;
+
+ if ( orig.width > max.width ) {
+ w_ratio = max.width / orig.width;
+ }
+ if ( orig.height > max.height ) {
+ h_ratio = max.height / orig.height;
+ }
+
+ if ( w_ratio < h_ratio ) {
+ width = max.width;
+ height = Math.floor( width / orig_ratio );
+ } else if ( h_ratio < w_ratio ) {
+ height = max.height;
+ width = Math.floor( height * orig_ratio );
+ } else {
+ width = orig.width;
+ height = orig.height;
+ }
+
+ return {
+ width: width,
+ height: height,
+ };
+ },
+
+ fitInfo: function(/*animated*/) {
+ var current = this.jp_carousel( 'selectedSlide' ),
+ size = current.jp_carousel( 'bestFit' );
+
+ photo_info.css( {
+ left: Math.floor( ( info.width() - size.width ) * 0.5 ),
+ width: Math.floor( size.width ),
+ } );
+
+ return this;
+ },
+
+ fitMeta: function( animated ) {
+ var newInfoTop = {
+ top: Math.floor( ( $( window ).height() / 100 ) * proportion + 5 ) + 'px',
+ };
+ var newLeftWidth = { width: info.width() - ( imageMeta.width() + 80 ) + 'px' };
+
+ if ( animated ) {
+ info.animate( newInfoTop );
+ leftColWrapper.animate( newLeftWidth );
+ } else {
+ info.animate( newInfoTop );
+ leftColWrapper.css( newLeftWidth );
+ }
+ },
+
+ fitSlide: function(/*animated*/) {
+ return this.each( function() {
+ var $this = $( this ),
+ dimensions = $this.jp_carousel( 'bestFit' ),
+ method = 'css',
+ max = gallery.jp_carousel( 'slideDimensions' );
+
+ dimensions.left = 0;
+ dimensions.top = Math.floor( ( max.height - dimensions.height ) * 0.5 ) + 40;
+ $this[ method ]( dimensions );
+ } );
+ },
+
+ texturize: function( text ) {
+ text = '' + text; // make sure we get a string. Title "1" came in as int 1, for example, which did not support .replace().
+ text = text
+ .replace( /'/g, '&#8217;' )
+ .replace( /&#039;/g, '&#8217;' )
+ .replace( /[\u2019]/g, '&#8217;' );
+ text = text
+ .replace( /"/g, '&#8221;' )
+ .replace( /&#034;/g, '&#8221;' )
+ .replace( /&quot;/g, '&#8221;' )
+ .replace( /[\u201D]/g, '&#8221;' );
+ text = text.replace( /([\w]+)=&#[\d]+;(.+?)&#[\d]+;/g, '$1="$2"' ); // untexturize allowed HTML tags params double-quotes
+ return $.trim( text );
+ },
+
+ initSlides: function( items, start_index ) {
+ if ( items.length < 2 ) {
+ $( '.jp-carousel-next-button, .jp-carousel-previous-button' ).hide();
+ } else {
+ $( '.jp-carousel-next-button, .jp-carousel-previous-button' ).show();
+ }
+
+ // Calculate the new src.
+ items.each( function(/*i*/) {
+ var src_item = $( this ),
+ orig_size = src_item.data( 'orig-size' ) || '',
+ max = gallery.jp_carousel( 'slideDimensions' ),
+ parts = orig_size.split( ',' ),
+ medium_file = src_item.data( 'medium-file' ) || '',
+ large_file = src_item.data( 'large-file' ) || '',
+ src;
+ orig_size = { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) };
+
+ src = src_item.data( 'orig-file' );
+
+ src = gallery.jp_carousel( 'selectBestImageSize', {
+ orig_file: src,
+ orig_width: orig_size.width,
+ orig_height: orig_size.height,
+ max_width: max.width,
+ max_height: max.height,
+ medium_file: medium_file,
+ large_file: large_file,
+ } );
+
+ // Set the final src
+ $( this ).data( 'gallery-src', src );
+ } );
+
+ // If the start_index is not 0 then preload the clicked image first.
+ if ( 0 !== start_index ) {
+ $( '<img/>' )[ 0 ].src = $( items[ start_index ] ).data( 'gallery-src' );
+ }
+
+ var useInPageThumbnails =
+ items.first().closest( '.tiled-gallery.type-rectangular' ).length > 0;
+
+ // create the 'slide'
+ items.each( function( i ) {
+ var src_item = $( this ),
+ attachment_id = src_item.data( 'attachment-id' ) || 0,
+ comments_opened = src_item.data( 'comments-opened' ) || 0,
+ image_meta = src_item.data( 'image-meta' ) || {},
+ orig_size = src_item.data( 'orig-size' ) || '',
+ thumb_size = { width: src_item[ 0 ].naturalWidth, height: src_item[ 0 ].naturalHeight },
+ title = src_item.data( 'image-title' ) || '',
+ description = src_item.data( 'image-description' ) || '',
+ caption =
+ src_item
+ .parents( '.gallery-item' )
+ .find( '.gallery-caption' )
+ .html() || '',
+ src = src_item.data( 'gallery-src' ) || '',
+ medium_file = src_item.data( 'medium-file' ) || '',
+ large_file = src_item.data( 'large-file' ) || '',
+ orig_file = src_item.data( 'orig-file' ) || '';
+
+ var tiledCaption = src_item
+ .parents( 'div.tiled-gallery-item' )
+ .find( 'div.tiled-gallery-caption' )
+ .html();
+ if ( tiledCaption ) {
+ caption = tiledCaption;
+ }
+
+ if ( attachment_id && orig_size.length ) {
+ title = gallery.jp_carousel( 'texturize', title );
+ description = gallery.jp_carousel( 'texturize', description );
+ caption = gallery.jp_carousel( 'texturize', caption );
+
+ // Initially, the image is a 1x1 transparent gif. The preview is shown as a background image on the slide itself.
+ var image = $( '<img/>' )
+ .attr(
+ 'src',
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
+ )
+ .css( 'width', '100%' )
+ .css( 'height', '100%' );
+
+ var slide = $(
+ '<div class="jp-carousel-slide" itemprop="associatedMedia" itemscope itemtype="https://schema.org/ImageObject"></div>'
+ )
+ .hide()
+ .css( {
+ //'position' : 'fixed',
+ left: i < start_index ? -1000 : gallery.width(),
+ } )
+ .append( image )
+ .appendTo( gallery )
+ .data( 'src', src )
+ .data( 'title', title )
+ .data( 'desc', description )
+ .data( 'caption', caption )
+ .data( 'attachment-id', attachment_id )
+ .data( 'permalink', src_item.parents( 'a' ).attr( 'href' ) )
+ .data( 'orig-size', orig_size )
+ .data( 'comments-opened', comments_opened )
+ .data( 'image-meta', image_meta )
+ .data( 'medium-file', medium_file )
+ .data( 'large-file', large_file )
+ .data( 'orig-file', orig_file )
+ .data( 'thumb-size', thumb_size );
+ if ( useInPageThumbnails ) {
+ // Use the image already loaded in the gallery as a preview.
+ slide.data( 'preview-image', src_item.attr( 'src' ) ).css( {
+ 'background-image': 'url("' + src_item.attr( 'src' ) + '")',
+ 'background-size': '100% 100%',
+ 'background-position': 'center center',
+ } );
+ }
+
+ slide.jp_carousel( 'fitSlide', false );
+ }
+ } );
+ return this;
+ },
+
+ selectBestImageSize: function( args ) {
+ if ( 'object' !== typeof args ) {
+ args = {};
+ }
+
+ if ( 'undefined' === typeof args.orig_file ) {
+ return '';
+ }
+
+ if ( 'undefined' === typeof args.orig_width || 'undefined' === typeof args.max_width ) {
+ return args.orig_file;
+ }
+
+ if ( 'undefined' === typeof args.medium_file || 'undefined' === typeof args.large_file ) {
+ return args.orig_file;
+ }
+
+ // Check if the image is being served by Photon (using a regular expression on the hostname).
+
+ var imageLinkParser = document.createElement( 'a' );
+ imageLinkParser.href = args.large_file;
+
+ var isPhotonUrl = /^i[0-2].wp.com$/i.test( imageLinkParser.hostname );
+
+ var medium_size_parts = gallery.jp_carousel(
+ 'getImageSizeParts',
+ args.medium_file,
+ args.orig_width,
+ isPhotonUrl
+ );
+ var large_size_parts = gallery.jp_carousel(
+ 'getImageSizeParts',
+ args.large_file,
+ args.orig_width,
+ isPhotonUrl
+ );
+
+ var large_width = parseInt( large_size_parts[ 0 ], 10 ),
+ large_height = parseInt( large_size_parts[ 1 ], 10 ),
+ medium_width = parseInt( medium_size_parts[ 0 ], 10 ),
+ medium_height = parseInt( medium_size_parts[ 1 ], 10 );
+
+ // Assign max width and height.
+ args.orig_max_width = args.max_width;
+ args.orig_max_height = args.max_height;
+
+ // Give devices with a higher devicePixelRatio higher-res images (Retina display = 2, Android phones = 1.5, etc)
+ if ( 'undefined' !== typeof window.devicePixelRatio && window.devicePixelRatio > 1 ) {
+ args.max_width = args.max_width * window.devicePixelRatio;
+ args.max_height = args.max_height * window.devicePixelRatio;
+ }
+
+ if ( large_width >= args.max_width || large_height >= args.max_height ) {
+ return args.large_file;
+ }
+
+ if ( medium_width >= args.max_width || medium_height >= args.max_height ) {
+ return args.medium_file;
+ }
+
+ if ( isPhotonUrl ) {
+ // args.orig_file doesn't point to a Photon url, so in this case we use args.large_file
+ // to return the photon url of the original image.
+ var largeFileIndex = args.large_file.lastIndexOf( '?' );
+ var origPhotonUrl = args.large_file;
+ if ( -1 !== largeFileIndex ) {
+ origPhotonUrl = args.large_file.substring( 0, largeFileIndex );
+ // If we have a really large image load a smaller version
+ // that is closer to the viewable size
+ if ( args.orig_width > args.max_width || args.orig_height > args.max_height ) {
+ origPhotonUrl += '?fit=' + args.orig_max_width + '%2C' + args.orig_max_height;
+ }
+ }
+ return origPhotonUrl;
+ }
+
+ return args.orig_file;
+ },
+
+ getImageSizeParts: function( file, orig_width, isPhotonUrl ) {
+ var size = isPhotonUrl
+ ? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' )
+ : file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' );
+
+ var size_parts =
+ size !== file
+ ? isPhotonUrl
+ ? size.split( '%2C' )
+ : size.split( 'x' )
+ : [ orig_width, 0 ];
+
+ // If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url.
+ // In that case, we set the value to 0.
+ if ( '9999' === size_parts[ 0 ] ) {
+ size_parts[ 0 ] = '0';
+ }
+
+ if ( '9999' === size_parts[ 1 ] ) {
+ size_parts[ 1 ] = '0';
+ }
+
+ return size_parts;
+ },
+
+ originalDimensions: function() {
+ var splitted = $( this )
+ .data( 'orig-size' )
+ .split( ',' );
+ return { width: parseInt( splitted[ 0 ], 10 ), height: parseInt( splitted[ 1 ], 10 ) };
+ },
+
+ format: function( args ) {
+ if ( 'object' !== typeof args ) {
+ args = {};
+ }
+ if ( ! args.text || 'undefined' === typeof args.text ) {
+ return;
+ }
+ if ( ! args.replacements || 'undefined' === typeof args.replacements ) {
+ return args.text;
+ }
+ return args.text.replace( /{(\d+)}/g, function( match, number ) {
+ return typeof args.replacements[ number ] !== 'undefined'
+ ? args.replacements[ number ]
+ : match;
+ } );
+ },
+
+ /**
+ * Returns a number in a fraction format that represents the shutter speed.
+ * @param Number speed
+ * @return String
+ */
+ shutterSpeed: function( speed ) {
+ var denominator;
+
+ // round to one decimal if value > 1s by multiplying it by 10, rounding, then dividing by 10 again
+ if ( speed >= 1 ) {
+ return Math.round( speed * 10 ) / 10 + 's';
+ }
+
+ // If the speed is less than one, we find the denominator by inverting
+ // the number. Since cameras usually use rational numbers as shutter
+ // speeds, we should get a nice round number. Or close to one in cases
+ // like 1/30. So we round it.
+ denominator = Math.round( 1 / speed );
+
+ return '1/' + denominator + 's';
+ },
+
+ parseTitleDesc: function( value ) {
+ if ( ! value.match( ' ' ) && value.match( '_' ) ) {
+ return '';
+ }
+
+ return value;
+ },
+
+ getTitleDesc: function( data ) {
+ var title = '',
+ desc = '',
+ markup = '',
+ target;
+
+ target = $( 'div.jp-carousel-titleanddesc', 'div.jp-carousel-wrap' );
+ target.hide();
+
+ title = gallery.jp_carousel( 'parseTitleDesc', data.title ) || '';
+ desc = gallery.jp_carousel( 'parseTitleDesc', data.desc ) || '';
+
+ if ( title.length || desc.length ) {
+ // Convert from HTML to plain text (including HTML entities decode, etc)
+ if (
+ $( '<div />' )
+ .html( title )
+ .text() ===
+ $( '<div />' )
+ .html( desc )
+ .text()
+ ) {
+ title = '';
+ }
+
+ markup = title.length
+ ? '<div class="jp-carousel-titleanddesc-title">' + title + '</div>'
+ : '';
+ markup += desc.length
+ ? '<div class="jp-carousel-titleanddesc-desc">' + desc + '</div>'
+ : '';
+
+ target.html( markup ).fadeIn( 'slow' );
+ }
+
+ $( 'div#jp-carousel-comment-form-container' ).css( 'margin-top', '20px' );
+ $( 'div#jp-carousel-comments-loading' ).css( 'margin-top', '20px' );
+ },
+
+ // updateExif updates the contents of the exif UL (.jp-carousel-image-exif)
+ updateExif: function( meta ) {
+ if ( ! meta || 1 !== Number( jetpackCarouselStrings.display_exif ) ) {
+ return false;
+ }
+
+ var $ul = $( "<ul class='jp-carousel-image-exif'></ul>" );
+
+ $.each( meta, function( key, val ) {
+ if (
+ 0 === parseFloat( val ) ||
+ ! val.length ||
+ -1 === $.inArray( key, $.makeArray( jetpackCarouselStrings.meta_data ) )
+ ) {
+ return;
+ }
+
+ switch ( key ) {
+ case 'focal_length':
+ val = val + 'mm';
+ break;
+ case 'shutter_speed':
+ val = gallery.jp_carousel( 'shutterSpeed', val );
+ break;
+ case 'aperture':
+ val = 'f/' + val;
+ break;
+ }
+
+ $ul.append( '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>' );
+ } );
+
+ // Update (replace) the content of the ul
+ $( 'div.jp-carousel-image-meta ul.jp-carousel-image-exif' ).replaceWith( $ul );
+ },
+
+ // updateFullSizeLink updates the contents of the jp-carousel-image-download link
+ updateFullSizeLink: function( current ) {
+ if ( ! current || ! current.data ) {
+ return false;
+ }
+ var original,
+ origSize = current.data( 'orig-size' ).split( ',' ),
+ imageLinkParser = document.createElement( 'a' );
+
+ imageLinkParser.href = current.data( 'src' ).replace( /\?.+$/, '' );
+
+ // Is this a Photon URL?
+ if ( imageLinkParser.hostname.match( /^i[\d]{1}.wp.com$/i ) !== null ) {
+ original = imageLinkParser.href;
+ } else {
+ original = current.data( 'orig-file' ).replace( /\?.+$/, '' );
+ }
+
+ var permalink = $(
+ '<a>' +
+ gallery.jp_carousel( 'format', {
+ text: jetpackCarouselStrings.download_original,
+ replacements: origSize,
+ } ) +
+ '</a>'
+ )
+ .addClass( 'jp-carousel-image-download' )
+ .attr( 'href', original )
+ .attr( 'target', '_blank' );
+
+ // Update (replace) the content of the anchor
+ $( 'div.jp-carousel-image-meta a.jp-carousel-image-download' ).replaceWith( permalink );
+ },
+
+ updateMap: function( meta ) {
+ if (
+ ! meta.latitude ||
+ ! meta.longitude ||
+ 1 !== Number( jetpackCarouselStrings.display_geo )
+ ) {
+ return;
+ }
+
+ var latitude = meta.latitude,
+ longitude = meta.longitude,
+ $metabox = $( 'div.jp-carousel-image-meta', 'div.jp-carousel-wrap' ),
+ $mapbox = $( '<div></div>' ),
+ style =
+ '&scale=2&style=feature:all|element:all|invert_lightness:true|hue:0x0077FF|saturation:-50|lightness:-5|gamma:0.91';
+
+ $mapbox
+ .addClass( 'jp-carousel-image-map' )
+ .html(
+ '<img width="154" height="154" src="https://maps.googleapis.com/maps/api/staticmap?\
+ center=' +
+ latitude +
+ ',' +
+ longitude +
+ '&\
+ zoom=8&\
+ size=154x154&\
+ sensor=false&\
+ markers=size:medium%7Ccolor:blue%7C' +
+ latitude +
+ ',' +
+ longitude +
+ style +
+ '" class="gmap-main" />\
+ \
+ <div class="gmap-topright"><div class="imgclip"><img width="175" height="154" src="https://maps.googleapis.com/maps/api/staticmap?\
+ center=' +
+ latitude +
+ ',' +
+ longitude +
+ '&\
+ zoom=3&\
+ size=175x154&\
+ sensor=false&\
+ markers=size:small%7Ccolor:blue%7C' +
+ latitude +
+ ',' +
+ longitude +
+ style +
+ '"c /></div></div>\
+ \
+ '
+ )
+ .prependTo( $metabox );
+ },
+
+ testCommentsOpened: function( opened ) {
+ if ( 1 === parseInt( opened, 10 ) ) {
+ $( '.jp-carousel-buttons' ).fadeIn( 'fast' );
+ commentForm.fadeIn( 'fast' );
+ } else {
+ $( '.jp-carousel-buttons' ).fadeOut( 'fast' );
+ commentForm.fadeOut( 'fast' );
+ }
+ },
+
+ getComments: function( args ) {
+ clearInterval( commentInterval );
+
+ if ( 'object' !== typeof args ) {
+ return;
+ }
+
+ if ( 'undefined' === typeof args.attachment_id || ! args.attachment_id ) {
+ return;
+ }
+
+ if ( ! args.offset || 'undefined' === typeof args.offset || args.offset < 1 ) {
+ args.offset = 0;
+ }
+
+ var comments = $( '.jp-carousel-comments' ),
+ commentsLoading = $( '#jp-carousel-comments-loading' ).show();
+
+ if ( args.clear ) {
+ comments.hide().empty();
+ }
+
+ $.ajax( {
+ type: 'GET',
+ url: jetpackCarouselStrings.ajaxurl,
+ dataType: 'json',
+ data: {
+ action: 'get_attachment_comments',
+ nonce: jetpackCarouselStrings.nonce,
+ id: args.attachment_id,
+ offset: args.offset,
+ },
+ success: function( data /*, status, xhr*/ ) {
+ if ( args.clear ) {
+ comments.fadeOut( 'fast' ).empty();
+ }
+
+ $( data ).each( function() {
+ var comment = $( '<div></div>' )
+ .addClass( 'jp-carousel-comment' )
+ .attr( 'id', 'jp-carousel-comment-' + this[ 'id' ] )
+ .html(
+ '<div class="comment-gravatar">' +
+ this[ 'gravatar_markup' ] +
+ '</div>' +
+ '<div class="comment-author">' +
+ this[ 'author_markup' ] +
+ '</div>' +
+ '<div class="comment-date">' +
+ this[ 'date_gmt' ] +
+ '</div>' +
+ '<div class="comment-content">' +
+ this[ 'content' ] +
+ '</div>'
+ );
+ comments.append( comment );
+
+ // Set the interval to check for a new page of comments.
+ clearInterval( commentInterval );
+ commentInterval = setInterval( function() {
+ if (
+ $( '.jp-carousel-overlay' ).height() - 150 <
+ $( '.jp-carousel-wrap' ).scrollTop() + $( window ).height()
+ ) {
+ gallery.jp_carousel( 'getComments', {
+ attachment_id: args.attachment_id,
+ offset: args.offset + 10,
+ clear: false,
+ } );
+ clearInterval( commentInterval );
+ }
+ }, 300 );
+ } );
+
+ // Verify (late) that the user didn't repeatldy click the arrows really fast, in which case the requested
+ // attachment id might no longer match the current attachment id by the time we get the data back or a now
+ // registered infiniscroll event kicks in, so we don't ever display comments for the wrong image by mistake.
+ var current = $( '.jp-carousel div.selected' );
+ if (
+ current &&
+ current.data &&
+ current.data( 'attachment-id' ) != args.attachment_id // jshint ignore:line
+ ) {
+ comments.fadeOut( 'fast' );
+ comments.empty();
+ return;
+ }
+
+ // Increase the height of the background, semi-transparent overlay to match the new length of the comments list.
+ $( '.jp-carousel-overlay' ).height(
+ $( window ).height() +
+ titleAndDescription.height() +
+ commentForm.height() +
+ ( comments.height() > 0 ? comments.height() : imageMeta.height() ) +
+ 200
+ );
+
+ comments.show();
+ commentsLoading.hide();
+ },
+ error: function( xhr, status, error ) {
+ // TODO: proper error handling
+ console.log( 'Comment get fail...', xhr, status, error );
+ comments.fadeIn( 'fast' );
+ commentsLoading.fadeOut( 'fast' );
+ },
+ } );
+ },
+
+ postCommentError: function( args ) {
+ if ( 'object' !== typeof args ) {
+ args = {};
+ }
+ if (
+ ! args.field ||
+ 'undefined' === typeof args.field ||
+ ! args.error ||
+ 'undefined' === typeof args.error
+ ) {
+ return;
+ }
+ $( '#jp-carousel-comment-post-results' )
+ .slideUp( 'fast' )
+ .html( '<span class="jp-carousel-comment-post-error">' + args.error + '</span>' )
+ .slideDown( 'fast' );
+ $( '#jp-carousel-comment-form-spinner' ).spin( false );
+ },
+
+ setCommentIframeSrc: function( attachment_id ) {
+ var iframe = $( '#jp-carousel-comment-iframe' );
+ // Set the proper irame src for the current attachment id
+ if ( iframe && iframe.length ) {
+ iframe.attr( 'src', iframe.attr( 'src' ).replace( /(postid=)\d+/, '$1' + attachment_id ) );
+ iframe.attr(
+ 'src',
+ iframe.attr( 'src' ).replace( /(%23.+)?$/, '%23jp-carousel-' + attachment_id )
+ );
+ }
+ },
+
+ clearCommentTextAreaValue: function() {
+ var commentTextArea = $( '#jp-carousel-comment-form-comment-field' );
+ if ( commentTextArea ) {
+ commentTextArea.val( '' );
+ }
+ },
+
+ nextSlide: function() {
+ var slides = this.jp_carousel( 'slides' );
+ var selected = this.jp_carousel( 'selectedSlide' );
+
+ if ( selected.length === 0 || ( slides.length > 2 && selected.is( slides.last() ) ) ) {
+ return slides.first();
+ }
+
+ return selected.next();
+ },
+
+ prevSlide: function() {
+ var slides = this.jp_carousel( 'slides' );
+ var selected = this.jp_carousel( 'selectedSlide' );
+
+ if ( selected.length === 0 || ( slides.length > 2 && selected.is( slides.first() ) ) ) {
+ return slides.last();
+ }
+
+ return selected.prev();
+ },
+
+ loadFullImage: function( slide ) {
+ var image = slide.find( 'img:first' );
+
+ if ( ! image.data( 'loaded' ) ) {
+ // If the width of the slide is smaller than the width of the "thumbnail" we're already using,
+ // don't load the full image.
+
+ image.on( 'load.jetpack', function() {
+ image.off( 'load.jetpack' );
+ $( this )
+ .closest( '.jp-carousel-slide' )
+ .css( 'background-image', '' );
+ } );
+
+ if (
+ ! slide.data( 'preview-image' ) ||
+ ( slide.data( 'thumb-size' ) && slide.width() > slide.data( 'thumb-size' ).width )
+ ) {
+ image
+ .attr( 'src', image.closest( '.jp-carousel-slide' ).data( 'src' ) )
+ .attr( 'itemprop', 'image' );
+ } else {
+ image.attr( 'src', slide.data( 'preview-image' ) ).attr( 'itemprop', 'image' );
+ }
+
+ image.data( 'loaded', 1 );
+ }
+ },
+
+ hasMultipleImages: function() {
+ return gallery.jp_carousel( 'slides' ).length > 1;
+ },
+ };
+
+ $.fn.jp_carousel = function( method ) {
+ // ask for the HTML of the gallery
+ // Method calling logic
+ if ( methods[ method ] ) {
+ return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) );
+ } else if ( typeof method === 'object' || ! method ) {
+ return methods.open.apply( this, arguments );
+ } else {
+ $.error( 'Method ' + method + ' does not exist on jQuery.jp_carousel' );
+ }
+ };
+
+ // register the event listener for starting the gallery
+ $( document.body ).on(
+ 'click.jp-carousel',
+ 'div.gallery, div.tiled-gallery, ul.wp-block-gallery, div.wp-block-jetpack-tiled-gallery, a.single-image-gallery',
+ function( e ) {
+ if ( ! $( this ).jp_carousel( 'testForData', e.currentTarget ) ) {
+ return;
+ }
+
+ // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link.
+ if (
+ $( e.target )
+ .parent()
+ .hasClass( 'gallery-caption' )
+ ) {
+ return;
+ }
+
+ // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link.
+ if (
+ $( e.target )
+ .parent()
+ .is( 'figcaption' )
+ ) {
+ return;
+ }
+
+ e.preventDefault();
+
+ // Stopping propagation in case there are parent elements
+ // with .gallery or .tiled-gallery class
+ e.stopPropagation();
+ $( this ).jp_carousel( 'open', {
+ start_index: $( this )
+ .find( '.gallery-item, .tiled-gallery-item, .blocks-gallery-item, .tiled-gallery__item' )
+ .index(
+ $( e.target ).parents(
+ '.gallery-item, .tiled-gallery-item, .blocks-gallery-item, .tiled-gallery__item'
+ )
+ ),
+ } );
+ }
+ );
+
+ // handle lightbox (single image gallery) for images linking to 'Attachment Page'
+ if ( 1 === Number( jetpackCarouselStrings.single_image_gallery ) ) {
+ processSingleImageGallery();
+ $( document.body ).on( 'post-load', function() {
+ processSingleImageGallery();
+ } );
+ }
+
+ // Makes carousel work on page load and when back button leads to same URL with carousel hash (ie: no actual document.ready trigger)
+ $( window ).on( 'hashchange.jp-carousel', function() {
+ var hashRegExp = /jp-carousel-(\d+)/,
+ matches,
+ attachmentId,
+ galleries,
+ selectedThumbnail;
+
+ if ( ! window.location.hash || ! hashRegExp.test( window.location.hash ) ) {
+ if ( gallery && gallery.opened ) {
+ container.jp_carousel( 'close' );
+ }
+
+ return;
+ }
+
+ if ( window.location.hash === last_known_location_hash && gallery.opened ) {
+ return;
+ }
+
+ if ( window.location.hash && gallery && ! gallery.opened && history.back ) {
+ history.back();
+ return;
+ }
+
+ last_known_location_hash = window.location.hash;
+ matches = window.location.hash.match( hashRegExp );
+ attachmentId = parseInt( matches[ 1 ], 10 );
+ galleries = $(
+ 'div.gallery, div.tiled-gallery, a.single-image-gallery, ul.wp-block-gallery, div.wp-block-jetpack-tiled-gallery'
+ );
+
+ // Find the first thumbnail that matches the attachment ID in the location
+ // hash, then open the gallery that contains it.
+ galleries.each( function( _, galleryEl ) {
+ $( galleryEl )
+ .find( 'img' )
+ .each( function( imageIndex, imageEl ) {
+ if ( $( imageEl ).data( 'attachment-id' ) === parseInt( attachmentId, 10 ) ) {
+ selectedThumbnail = { index: imageIndex, gallery: galleryEl };
+ return false;
+ }
+ } );
+
+ if ( selectedThumbnail ) {
+ $( selectedThumbnail.gallery ).jp_carousel( 'openOrSelectSlide', selectedThumbnail.index );
+ return false;
+ }
+ } );
+ } );
+
+ if ( window.location.hash ) {
+ $( window ).trigger( 'hashchange' );
+ }
+} );
+
+/**
+ * jQuery Plugin to obtain touch gestures from iPhone, iPod Touch and iPad, should also work with Android mobile phones (not tested yet!)
+ * Common usage: wipe images (left and right to show the previous or next image)
+ *
+ * @author Andreas Waltl, netCU Internetagentur (http://www.netcu.de)
+ * Version 1.1.1, modified to pass the touchmove event to the callbacks.
+ */
+( function( $ ) {
+ $.fn.touchwipe = function( settings ) {
+ var config = {
+ min_move_x: 20,
+ min_move_y: 20,
+ wipeLeft: function(/*e*/) {},
+ wipeRight: function(/*e*/) {},
+ wipeUp: function(/*e*/) {},
+ wipeDown: function(/*e*/) {},
+ preventDefaultEvents: true,
+ };
+
+ if ( settings ) {
+ $.extend( config, settings );
+ }
+
+ this.each( function() {
+ var startX;
+ var startY;
+ var isMoving = false;
+
+ function cancelTouch() {
+ this.removeEventListener( 'touchmove', onTouchMove );
+ startX = null;
+ isMoving = false;
+ }
+
+ function onTouchMove( e ) {
+ if ( config.preventDefaultEvents ) {
+ e.preventDefault();
+ }
+ if ( isMoving ) {
+ var x = e.touches[ 0 ].pageX;
+ var y = e.touches[ 0 ].pageY;
+ var dx = startX - x;
+ var dy = startY - y;
+ if ( Math.abs( dx ) >= config.min_move_x ) {
+ cancelTouch();
+ if ( dx > 0 ) {
+ config.wipeLeft( e );
+ } else {
+ config.wipeRight( e );
+ }
+ } else if ( Math.abs( dy ) >= config.min_move_y ) {
+ cancelTouch();
+ if ( dy > 0 ) {
+ config.wipeDown( e );
+ } else {
+ config.wipeUp( e );
+ }
+ }
+ }
+ }
+
+ function onTouchStart( e ) {
+ if ( e.touches.length === 1 ) {
+ startX = e.touches[ 0 ].pageX;
+ startY = e.touches[ 0 ].pageY;
+ isMoving = true;
+ this.addEventListener( 'touchmove', onTouchMove, false );
+ }
+ }
+ if ( 'ontouchstart' in document.documentElement ) {
+ this.addEventListener( 'touchstart', onTouchStart, false );
+ }
+ } );
+
+ return this;
+ };
+} )( jQuery );
diff --git a/plugins/jetpack/modules/carousel/jetpack-carousel.php b/plugins/jetpack/modules/carousel/jetpack-carousel.php
new file mode 100644
index 00000000..1542a297
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/jetpack-carousel.php
@@ -0,0 +1,832 @@
+<?php
+
+/*
+Plugin Name: Jetpack Carousel
+Plugin URL: https://wordpress.com/
+Description: Transform your standard image galleries into an immersive full-screen experience.
+Version: 0.1
+Author: Automattic
+
+Released under the GPL v.2 license.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+*/
+class Jetpack_Carousel {
+
+ public $prebuilt_widths = array( 370, 700, 1000, 1200, 1400, 2000 );
+
+ public $first_run = true;
+
+ public $in_gallery = false;
+
+ public $in_jetpack = true;
+
+ public $single_image_gallery_enabled = false;
+
+ public $single_image_gallery_enabled_media_file = false;
+
+ function __construct() {
+ add_action( 'init', array( $this, 'init' ) );
+ }
+
+ function init() {
+ if ( $this->maybe_disable_jp_carousel() ) {
+ return;
+ }
+
+ $this->in_jetpack = ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'enable_module_configurable' ) ) ? true : false;
+
+ $this->single_image_gallery_enabled = ! $this->maybe_disable_jp_carousel_single_images();
+ $this->single_image_gallery_enabled_media_file = $this->maybe_enable_jp_carousel_single_images_media_file();
+
+ if ( is_admin() ) {
+ // Register the Carousel-related related settings
+ add_action( 'admin_init', array( $this, 'register_settings' ), 5 );
+ if ( ! $this->in_jetpack ) {
+ if ( 0 == $this->test_1or0_option( get_option( 'carousel_enable_it' ), true ) ) {
+ return; // Carousel disabled, abort early, but still register setting so user can switch it back on
+ }
+ }
+ // If in admin, register the ajax endpoints.
+ add_action( 'wp_ajax_get_attachment_comments', array( $this, 'get_attachment_comments' ) );
+ add_action( 'wp_ajax_nopriv_get_attachment_comments', array( $this, 'get_attachment_comments' ) );
+ add_action( 'wp_ajax_post_attachment_comment', array( $this, 'post_attachment_comment' ) );
+ add_action( 'wp_ajax_nopriv_post_attachment_comment', array( $this, 'post_attachment_comment' ) );
+ } else {
+ if ( ! $this->in_jetpack ) {
+ if ( 0 == $this->test_1or0_option( get_option( 'carousel_enable_it' ), true ) ) {
+ return; // Carousel disabled, abort early
+ }
+ }
+ // If on front-end, do the Carousel thang.
+ /**
+ * Filter the array of default prebuilt widths used in Carousel.
+ *
+ * @module carousel
+ *
+ * @since 1.6.0
+ *
+ * @param array $this->prebuilt_widths Array of default widths.
+ */
+ $this->prebuilt_widths = apply_filters( 'jp_carousel_widths', $this->prebuilt_widths );
+ // below: load later than other callbacks hooked it (e.g. 3rd party plugins handling gallery shortcode)
+ add_filter( 'post_gallery', array( $this, 'check_if_shortcode_processed_and_enqueue_assets' ), 1000, 2 );
+ add_filter( 'post_gallery', array( $this, 'set_in_gallery' ), -1000 );
+ add_filter( 'gallery_style', array( $this, 'add_data_to_container' ) );
+ add_filter( 'wp_get_attachment_image_attributes', array( $this, 'add_data_to_images' ), 10, 2 );
+ add_filter( 'the_content', array( $this, 'check_content_for_blocks' ), 1 );
+ add_filter( 'jetpack_tiled_galleries_block_content', array( $this, 'add_data_img_tags_and_enqueue_assets' ) );
+ if ( $this->single_image_gallery_enabled ) {
+ add_filter( 'the_content', array( $this, 'add_data_img_tags_and_enqueue_assets' ) );
+ }
+ }
+
+ if ( $this->in_jetpack ) {
+ Jetpack::enable_module_configurable( dirname( dirname( __FILE__ ) ) . '/carousel.php' );
+ }
+ }
+
+ function maybe_disable_jp_carousel() {
+ /**
+ * Allow third-party plugins or themes to disable Carousel.
+ *
+ * @module carousel
+ *
+ * @since 1.6.0
+ *
+ * @param bool false Should Carousel be disabled? Default to false.
+ */
+ return apply_filters( 'jp_carousel_maybe_disable', false );
+ }
+
+ function maybe_disable_jp_carousel_single_images() {
+ /**
+ * Allow third-party plugins or themes to disable Carousel for single images.
+ *
+ * @module carousel
+ *
+ * @since 4.5.0
+ *
+ * @param bool false Should Carousel be disabled for single images? Default to false.
+ */
+ return apply_filters( 'jp_carousel_maybe_disable_single_images', false );
+ }
+
+ function maybe_enable_jp_carousel_single_images_media_file() {
+ /**
+ * Allow third-party plugins or themes to enable Carousel
+ * for single images linking to 'Media File' (full size image).
+ *
+ * @module carousel
+ *
+ * @since 4.5.0
+ *
+ * @param bool false Should Carousel be enabled for single images linking to 'Media File'? Default to false.
+ */
+ return apply_filters( 'jp_carousel_load_for_images_linked_to_file', false );
+ }
+
+ function asset_version( $version ) {
+ /**
+ * Filter the version string used when enqueuing Carousel assets.
+ *
+ * @module carousel
+ *
+ * @since 1.6.0
+ *
+ * @param string $version Asset version.
+ */
+ return apply_filters( 'jp_carousel_asset_version', $version );
+ }
+
+ function display_bail_message( $output = '' ) {
+ // Displays a message on top of gallery if carousel has bailed
+ $message = '<div class="jp-carousel-msg"><p>';
+ $message .= __( 'Jetpack\'s Carousel has been disabled, because another plugin or your theme is overriding the [gallery] shortcode.', 'jetpack' );
+ $message .= '</p></div>';
+ // put before gallery output
+ $output = $message . $output;
+ return $output;
+ }
+
+ function check_if_shortcode_processed_and_enqueue_assets( $output ) {
+ if (
+ class_exists( 'Jetpack_AMP_Support' )
+ && Jetpack_AMP_Support::is_amp_request()
+ ) {
+ return $output;
+ }
+
+ if (
+ ! empty( $output ) &&
+ /**
+ * Allow third-party plugins or themes to force-enable Carousel.
+ *
+ * @module carousel
+ *
+ * @since 1.9.0
+ *
+ * @param bool false Should we force enable Carousel? Default to false.
+ */
+ ! apply_filters( 'jp_carousel_force_enable', false )
+ ) {
+ // Bail because someone is overriding the [gallery] shortcode.
+ remove_filter( 'gallery_style', array( $this, 'add_data_to_container' ) );
+ remove_filter( 'wp_get_attachment_image_attributes', array( $this, 'add_data_to_images' ) );
+ remove_filter( 'the_content', array( $this, 'add_data_img_tags_and_enqueue_assets' ) );
+ // Display message that carousel has bailed, if user is super_admin, and if we're not on WordPress.com.
+ if (
+ is_super_admin() &&
+ ! ( defined( 'IS_WPCOM' ) && IS_WPCOM )
+ ) {
+ add_filter( 'post_gallery', array( $this, 'display_bail_message' ) );
+ }
+ return $output;
+ }
+
+ /**
+ * Fires when thumbnails are shown in Carousel.
+ *
+ * @module carousel
+ *
+ * @since 1.6.0
+ **/
+ do_action( 'jp_carousel_thumbnails_shown' );
+
+ $this->enqueue_assets();
+
+ return $output;
+ }
+
+ /**
+ * Check if the content of a post uses gallery blocks. To be used by 'the_content' filter.
+ *
+ * @since 6.8.0
+ *
+ * @param string $content Post content.
+ *
+ * @return string $content Post content.
+ */
+ function check_content_for_blocks( $content ) {
+ if (
+ class_exists( 'Jetpack_AMP_Support' )
+ && Jetpack_AMP_Support::is_amp_request()
+ ) {
+ return $content;
+ }
+
+ if ( has_block( 'gallery', $content ) || has_block( 'jetpack/tiled-gallery', $content ) ) {
+ $this->enqueue_assets();
+ $content = $this->add_data_to_container( $content );
+ }
+ return $content;
+ }
+
+ function enqueue_assets() {
+ if ( $this->first_run ) {
+ wp_enqueue_script(
+ 'jetpack-carousel',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/carousel/jetpack-carousel.min.js',
+ 'modules/carousel/jetpack-carousel.js'
+ ),
+ array( 'jquery.spin' ),
+ $this->asset_version( '20190102' ),
+ true
+ );
+
+ // Note: using home_url() instead of admin_url() for ajaxurl to be sure to get same domain on wpcom when using mapped domains (also works on self-hosted)
+ // Also: not hardcoding path since there is no guarantee site is running on site root in self-hosted context.
+ $is_logged_in = is_user_logged_in();
+ $current_user = wp_get_current_user();
+ $comment_registration = intval( get_option( 'comment_registration' ) );
+ $require_name_email = intval( get_option( 'require_name_email' ) );
+ $localize_strings = array(
+ 'widths' => $this->prebuilt_widths,
+ 'is_logged_in' => $is_logged_in,
+ 'lang' => strtolower( substr( get_locale(), 0, 2 ) ),
+ 'ajaxurl' => set_url_scheme( admin_url( 'admin-ajax.php' ) ),
+ 'nonce' => wp_create_nonce( 'carousel_nonce' ),
+ 'display_exif' => $this->test_1or0_option( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_display_exif', true ) ),
+ 'display_geo' => $this->test_1or0_option( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_display_geo', true ) ),
+ 'single_image_gallery' => $this->single_image_gallery_enabled,
+ 'single_image_gallery_media_file' => $this->single_image_gallery_enabled_media_file,
+ 'background_color' => $this->carousel_background_color_sanitize( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_background_color', '' ) ),
+ 'comment' => __( 'Comment', 'jetpack' ),
+ 'post_comment' => __( 'Post Comment', 'jetpack' ),
+ 'write_comment' => __( 'Write a Comment...', 'jetpack' ),
+ 'loading_comments' => __( 'Loading Comments...', 'jetpack' ),
+ 'download_original' => sprintf( __( 'View full size <span class="photo-size">%1$s<span class="photo-size-times">&times;</span>%2$s</span>', 'jetpack' ), '{0}', '{1}' ),
+ 'no_comment_text' => __( 'Please be sure to submit some text with your comment.', 'jetpack' ),
+ 'no_comment_email' => __( 'Please provide an email address to comment.', 'jetpack' ),
+ 'no_comment_author' => __( 'Please provide your name to comment.', 'jetpack' ),
+ 'comment_post_error' => __( 'Sorry, but there was an error posting your comment. Please try again later.', 'jetpack' ),
+ 'comment_approved' => __( 'Your comment was approved.', 'jetpack' ),
+ 'comment_unapproved' => __( 'Your comment is in moderation.', 'jetpack' ),
+ 'camera' => __( 'Camera', 'jetpack' ),
+ 'aperture' => __( 'Aperture', 'jetpack' ),
+ 'shutter_speed' => __( 'Shutter Speed', 'jetpack' ),
+ 'focal_length' => __( 'Focal Length', 'jetpack' ),
+ 'copyright' => __( 'Copyright', 'jetpack' ),
+ 'comment_registration' => $comment_registration,
+ 'require_name_email' => $require_name_email,
+ /** This action is documented in core/src/wp-includes/link-template.php */
+ 'login_url' => wp_login_url( apply_filters( 'the_permalink', get_permalink() ) ),
+ 'blog_id' => (int) get_current_blog_id(),
+ 'meta_data' => array( 'camera', 'aperture', 'shutter_speed', 'focal_length', 'copyright' ),
+ );
+
+ if ( ! isset( $localize_strings['jetpack_comments_iframe_src'] ) || empty( $localize_strings['jetpack_comments_iframe_src'] ) ) {
+ // We're not using Comments after all, so fallback to standard local comments.
+
+ if ( $is_logged_in ) {
+ $localize_strings['local_comments_commenting_as'] = '<p id="jp-carousel-commenting-as">' . sprintf( __( 'Commenting as %s', 'jetpack' ), $current_user->data->display_name ) . '</p>';
+ } else {
+ if ( $comment_registration ) {
+ $localize_strings['local_comments_commenting_as'] = '<p id="jp-carousel-commenting-as">' . __( 'You must be <a href="#" class="jp-carousel-comment-login">logged in</a> to post a comment.', 'jetpack' ) . '</p>';
+ } else {
+ $required = ( $require_name_email ) ? __( '%s (Required)', 'jetpack' ) : '%s';
+ $localize_strings['local_comments_commenting_as'] = ''
+ . '<fieldset><label for="email">' . sprintf( $required, __( 'Email', 'jetpack' ) ) . '</label> '
+ . '<input type="text" name="email" class="jp-carousel-comment-form-field jp-carousel-comment-form-text-field" id="jp-carousel-comment-form-email-field" /></fieldset>'
+ . '<fieldset><label for="author">' . sprintf( $required, __( 'Name', 'jetpack' ) ) . '</label> '
+ . '<input type="text" name="author" class="jp-carousel-comment-form-field jp-carousel-comment-form-text-field" id="jp-carousel-comment-form-author-field" /></fieldset>'
+ . '<fieldset><label for="url">' . __( 'Website', 'jetpack' ) . '</label> '
+ . '<input type="text" name="url" class="jp-carousel-comment-form-field jp-carousel-comment-form-text-field" id="jp-carousel-comment-form-url-field" /></fieldset>';
+ }
+ }
+ }
+
+ /**
+ * Handle WP stats for images in full-screen.
+ * Build string with tracking info.
+ */
+
+ /**
+ * Filter if Jetpack should enable stats collection on carousel views
+ *
+ * @module carousel
+ *
+ * @since 4.3.2
+ *
+ * @param bool Enable Jetpack Carousel stat collection. Default false.
+ */
+ if ( apply_filters( 'jetpack_enable_carousel_stats', false ) && in_array( 'stats', Jetpack::get_active_modules() ) && ! Jetpack::is_development_mode() ) {
+ $localize_strings['stats'] = 'blog=' . Jetpack_Options::get_option( 'id' ) . '&host=' . parse_url( get_option( 'home' ), PHP_URL_HOST ) . '&v=ext&j=' . JETPACK__API_VERSION . ':' . JETPACK__VERSION;
+
+ // Set the stats as empty if user is logged in but logged-in users shouldn't be tracked.
+ if ( is_user_logged_in() && function_exists( 'stats_get_options' ) ) {
+ $stats_options = stats_get_options();
+ $track_loggedin_users = isset( $stats_options['reg_users'] ) ? (bool) $stats_options['reg_users'] : false;
+
+ if ( ! $track_loggedin_users ) {
+ $localize_strings['stats'] = '';
+ }
+ }
+ }
+
+ /**
+ * Filter the strings passed to the Carousel's js file.
+ *
+ * @module carousel
+ *
+ * @since 1.6.0
+ *
+ * @param array $localize_strings Array of strings passed to the Jetpack js file.
+ */
+ $localize_strings = apply_filters( 'jp_carousel_localize_strings', $localize_strings );
+ wp_localize_script( 'jetpack-carousel', 'jetpackCarouselStrings', $localize_strings );
+ wp_enqueue_style( 'jetpack-carousel', plugins_url( 'jetpack-carousel.css', __FILE__ ), array(), $this->asset_version( '20120629' ) );
+ wp_style_add_data( 'jetpack-carousel', 'rtl', 'replace' );
+
+ /**
+ * Fires after carousel assets are enqueued for the first time.
+ * Allows for adding additional assets to the carousel page.
+ *
+ * @module carousel
+ *
+ * @since 1.6.0
+ *
+ * @param bool $first_run First load if Carousel on the page.
+ * @param array $localized_strings Array of strings passed to the Jetpack js file.
+ */
+ do_action( 'jp_carousel_enqueue_assets', $this->first_run, $localize_strings );
+
+ $this->first_run = false;
+ }
+ }
+
+ function set_in_gallery( $output ) {
+ if (
+ class_exists( 'Jetpack_AMP_Support' )
+ && Jetpack_AMP_Support::is_amp_request()
+ ) {
+ return $output;
+ }
+ $this->in_gallery = true;
+ return $output;
+ }
+
+ /**
+ * Adds data-* attributes required by carousel to img tags in post HTML
+ * content. To be used by 'the_content' filter.
+ *
+ * @see add_data_to_images()
+ * @see wp_make_content_images_responsive() in wp-includes/media.php
+ *
+ * @param string $content HTML content of the post
+ * @return string Modified HTML content of the post
+ */
+ function add_data_img_tags_and_enqueue_assets( $content ) {
+ if (
+ class_exists( 'Jetpack_AMP_Support' )
+ && Jetpack_AMP_Support::is_amp_request()
+ ) {
+ return $content;
+ }
+
+ if ( ! preg_match_all( '/<img [^>]+>/', $content, $matches ) ) {
+ return $content;
+ }
+ $selected_images = array();
+ foreach ( $matches[0] as $image_html ) {
+ if ( preg_match( '/(wp-image-|data-id=)\"?([0-9]+)\"?/i', $image_html, $class_id ) &&
+ ! preg_match( '/wp-block-jetpack-slideshow_image/', $image_html ) ) {
+ $attachment_id = absint( $class_id[2] );
+ /**
+ * If exactly the same image tag is used more than once, overwrite it.
+ * All identical tags will be replaced later with 'str_replace()'.
+ */
+ $selected_images[ $attachment_id ] = $image_html;
+ }
+ }
+
+ $find = array();
+ $replace = array();
+ if ( empty( $selected_images ) ) {
+ return $content;
+ }
+
+ $attachments = get_posts(
+ array(
+ 'include' => array_keys( $selected_images ),
+ 'post_type' => 'any',
+ 'post_status' => 'any',
+ 'suppress_filters' => false,
+ )
+ );
+
+ foreach ( $attachments as $attachment ) {
+ $image_html = $selected_images[ $attachment->ID ];
+
+ $attributes = $this->add_data_to_images( array(), $attachment );
+ $attributes_html = '';
+ foreach ( $attributes as $k => $v ) {
+ $attributes_html .= esc_attr( $k ) . '="' . esc_attr( $v ) . '" ';
+ }
+
+ $find[] = $image_html;
+ $replace[] = str_replace( '<img ', "<img $attributes_html", $image_html );
+ }
+
+ $content = str_replace( $find, $replace, $content );
+ $this->enqueue_assets();
+ return $content;
+ }
+
+ function add_data_to_images( $attr, $attachment = null ) {
+ if (
+ class_exists( 'Jetpack_AMP_Support' )
+ && Jetpack_AMP_Support::is_amp_request()
+ ) {
+ return $attr;
+ }
+
+ $attachment_id = intval( $attachment->ID );
+ if ( ! wp_attachment_is_image( $attachment_id ) ) {
+ return $attr;
+ }
+
+ $orig_file = wp_get_attachment_image_src( $attachment_id, 'full' );
+ $orig_file = isset( $orig_file[0] ) ? $orig_file[0] : wp_get_attachment_url( $attachment_id );
+ $meta = wp_get_attachment_metadata( $attachment_id );
+ $size = isset( $meta['width'] ) ? intval( $meta['width'] ) . ',' . intval( $meta['height'] ) : '';
+ $img_meta = ( ! empty( $meta['image_meta'] ) ) ? (array) $meta['image_meta'] : array();
+ $comments_opened = intval( comments_open( $attachment_id ) );
+
+ /**
+ * Note: Cannot generate a filename from the width and height wp_get_attachment_image_src() returns because
+ * it takes the $content_width global variable themes can set in consideration, therefore returning sizes
+ * which when used to generate a filename will likely result in a 404 on the image.
+ * $content_width has no filter we could temporarily de-register, run wp_get_attachment_image_src(), then
+ * re-register. So using returned file URL instead, which we can define the sizes from through filename
+ * parsing in the JS, as this is a failsafe file reference.
+ *
+ * EG with Twenty Eleven activated:
+ * array(4) { [0]=> string(82) "http://vanillawpinstall.blah/wp-content/uploads/2012/06/IMG_3534-1024x764.jpg" [1]=> int(584) [2]=> int(435) [3]=> bool(true) }
+ *
+ * EG with Twenty Ten activated:
+ * array(4) { [0]=> string(82) "http://vanillawpinstall.blah/wp-content/uploads/2012/06/IMG_3534-1024x764.jpg" [1]=> int(640) [2]=> int(477) [3]=> bool(true) }
+ */
+
+ $medium_file_info = wp_get_attachment_image_src( $attachment_id, 'medium' );
+ $medium_file = isset( $medium_file_info[0] ) ? $medium_file_info[0] : '';
+
+ $large_file_info = wp_get_attachment_image_src( $attachment_id, 'large' );
+ $large_file = isset( $large_file_info[0] ) ? $large_file_info[0] : '';
+
+ $attachment = get_post( $attachment_id );
+ $attachment_title = wptexturize( $attachment->post_title );
+ $attachment_desc = wpautop( wptexturize( $attachment->post_content ) );
+ // Not yet providing geo-data, need to "fuzzify" for privacy
+ if ( ! empty( $img_meta ) ) {
+ foreach ( $img_meta as $k => $v ) {
+ if ( 'latitude' == $k || 'longitude' == $k ) {
+ unset( $img_meta[ $k ] );
+ }
+ }
+ }
+
+ // See https://github.com/Automattic/jetpack/issues/2765
+ if ( isset( $img_meta['keywords'] ) ) {
+ unset( $img_meta['keywords'] );
+ }
+
+ $img_meta = json_encode( array_map( 'strval', array_filter( $img_meta, 'is_scalar' ) ) );
+
+ $attr['data-attachment-id'] = $attachment_id;
+ $attr['data-permalink'] = esc_attr( get_permalink( $attachment->ID ) );
+ $attr['data-orig-file'] = esc_attr( $orig_file );
+ $attr['data-orig-size'] = $size;
+ $attr['data-comments-opened'] = $comments_opened;
+ $attr['data-image-meta'] = esc_attr( $img_meta );
+ $attr['data-image-title'] = esc_attr( htmlspecialchars( $attachment_title ) );
+ $attr['data-image-description'] = esc_attr( htmlspecialchars( $attachment_desc ) );
+ $attr['data-medium-file'] = esc_attr( $medium_file );
+ $attr['data-large-file'] = esc_attr( $large_file );
+
+ return $attr;
+ }
+
+ function add_data_to_container( $html ) {
+ global $post;
+ if (
+ class_exists( 'Jetpack_AMP_Support' )
+ && Jetpack_AMP_Support::is_amp_request()
+ ) {
+ return $html;
+ }
+
+ if ( isset( $post ) ) {
+ $blog_id = (int) get_current_blog_id();
+
+ $extra_data = array(
+ 'data-carousel-extra' => array(
+ 'blog_id' => $blog_id,
+ 'permalink' => get_permalink( $post->ID ),
+ ),
+ );
+
+ /**
+ * Filter the data added to the Gallery container.
+ *
+ * @module carousel
+ *
+ * @since 1.6.0
+ *
+ * @param array $extra_data Array of data about the site and the post.
+ */
+ $extra_data = apply_filters( 'jp_carousel_add_data_to_container', $extra_data );
+ foreach ( (array) $extra_data as $data_key => $data_values ) {
+ $html = str_replace( '<div ', '<div ' . esc_attr( $data_key ) . "='" . json_encode( $data_values ) . "' ", $html );
+ $html = str_replace( '<ul class="wp-block-gallery', '<ul ' . esc_attr( $data_key ) . "='" . json_encode( $data_values ) . "' class=\"wp-block-gallery", $html );
+ }
+ }
+
+ return $html;
+ }
+
+ function get_attachment_comments() {
+ if ( ! headers_sent() ) {
+ header( 'Content-type: text/javascript' );
+ }
+
+ /**
+ * Allows for the checking of privileges of the blog user before comments
+ * are packaged as JSON and sent back from the get_attachment_comments
+ * AJAX endpoint
+ *
+ * @module carousel
+ *
+ * @since 1.6.0
+ */
+ do_action( 'jp_carousel_check_blog_user_privileges' );
+
+ $attachment_id = ( isset( $_REQUEST['id'] ) ) ? (int) $_REQUEST['id'] : 0;
+ $offset = ( isset( $_REQUEST['offset'] ) ) ? (int) $_REQUEST['offset'] : 0;
+
+ if ( ! $attachment_id ) {
+ echo json_encode( __( 'Missing attachment ID.', 'jetpack' ) );
+ die();
+ }
+
+ if ( $offset < 1 ) {
+ $offset = 0;
+ }
+
+ $comments = get_comments(
+ array(
+ 'status' => 'approve',
+ 'order' => ( 'asc' == get_option( 'comment_order' ) ) ? 'ASC' : 'DESC',
+ 'number' => 10,
+ 'offset' => $offset,
+ 'post_id' => $attachment_id,
+ )
+ );
+
+ $out = array();
+
+ // Can't just send the results, they contain the commenter's email address.
+ foreach ( $comments as $comment ) {
+ $avatar = get_avatar( $comment->comment_author_email, 64 );
+ if ( ! $avatar ) {
+ $avatar = '';
+ }
+ $out[] = array(
+ 'id' => $comment->comment_ID,
+ 'parent_id' => $comment->comment_parent,
+ 'author_markup' => get_comment_author_link( $comment->comment_ID ),
+ 'gravatar_markup' => $avatar,
+ 'date_gmt' => $comment->comment_date_gmt,
+ 'content' => wpautop( $comment->comment_content ),
+ );
+ }
+
+ die( json_encode( $out ) );
+ }
+
+ function post_attachment_comment() {
+ if ( ! headers_sent() ) {
+ header( 'Content-type: text/javascript' );
+ }
+
+ if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'carousel_nonce' ) ) {
+ die( json_encode( array( 'error' => __( 'Nonce verification failed.', 'jetpack' ) ) ) );
+ }
+
+ $_blog_id = (int) $_POST['blog_id'];
+ $_post_id = (int) $_POST['id'];
+ $comment = $_POST['comment'];
+
+ if ( empty( $_blog_id ) ) {
+ die( json_encode( array( 'error' => __( 'Missing target blog ID.', 'jetpack' ) ) ) );
+ }
+
+ if ( empty( $_post_id ) ) {
+ die( json_encode( array( 'error' => __( 'Missing target post ID.', 'jetpack' ) ) ) );
+ }
+
+ if ( empty( $comment ) ) {
+ die( json_encode( array( 'error' => __( 'No comment text was submitted.', 'jetpack' ) ) ) );
+ }
+
+ // Used in context like NewDash
+ $switched = false;
+ if ( is_multisite() && $_blog_id != get_current_blog_id() ) {
+ switch_to_blog( $_blog_id );
+ $switched = true;
+ }
+
+ /** This action is documented in modules/carousel/jetpack-carousel.php */
+ do_action( 'jp_carousel_check_blog_user_privileges' );
+
+ if ( ! comments_open( $_post_id ) ) {
+ die( json_encode( array( 'error' => __( 'Comments on this post are closed.', 'jetpack' ) ) ) );
+ }
+
+ if ( is_user_logged_in() ) {
+ $user = wp_get_current_user();
+ $user_id = $user->ID;
+ $display_name = $user->display_name;
+ $email = $user->user_email;
+ $url = $user->user_url;
+
+ if ( empty( $user_id ) ) {
+ die( json_encode( array( 'error' => __( 'Sorry, but we could not authenticate your request.', 'jetpack' ) ) ) );
+ }
+ } else {
+ $user_id = 0;
+ $display_name = $_POST['author'];
+ $email = $_POST['email'];
+ $url = $_POST['url'];
+
+ if ( get_option( 'require_name_email' ) ) {
+ if ( empty( $display_name ) ) {
+ die( json_encode( array( 'error' => __( 'Please provide your name.', 'jetpack' ) ) ) );
+ }
+
+ if ( empty( $email ) ) {
+ die( json_encode( array( 'error' => __( 'Please provide an email address.', 'jetpack' ) ) ) );
+ }
+
+ if ( ! is_email( $email ) ) {
+ die( json_encode( array( 'error' => __( 'Please provide a valid email address.', 'jetpack' ) ) ) );
+ }
+ }
+ }
+
+ $comment_data = array(
+ 'comment_content' => $comment,
+ 'comment_post_ID' => $_post_id,
+ 'comment_author' => $display_name,
+ 'comment_author_email' => $email,
+ 'comment_author_url' => $url,
+ 'comment_approved' => 0,
+ 'comment_type' => '',
+ );
+
+ if ( ! empty( $user_id ) ) {
+ $comment_data['user_id'] = $user_id;
+ }
+
+ // Note: wp_new_comment() sanitizes and validates the values (too).
+ $comment_id = wp_new_comment( $comment_data );
+
+ /**
+ * Fires before adding a new comment to the database via the get_attachment_comments ajax endpoint.
+ *
+ * @module carousel
+ *
+ * @since 1.6.0
+ */
+ do_action( 'jp_carousel_post_attachment_comment' );
+ $comment_status = wp_get_comment_status( $comment_id );
+
+ if ( true == $switched ) {
+ restore_current_blog();
+ }
+
+ die(
+ json_encode(
+ array(
+ 'comment_id' => $comment_id,
+ 'comment_status' => $comment_status,
+ )
+ )
+ );
+ }
+
+ function register_settings() {
+ add_settings_section( 'carousel_section', __( 'Image Gallery Carousel', 'jetpack' ), array( $this, 'carousel_section_callback' ), 'media' );
+
+ if ( ! $this->in_jetpack ) {
+ add_settings_field( 'carousel_enable_it', __( 'Enable carousel', 'jetpack' ), array( $this, 'carousel_enable_it_callback' ), 'media', 'carousel_section' );
+ register_setting( 'media', 'carousel_enable_it', array( $this, 'carousel_enable_it_sanitize' ) );
+ }
+
+ add_settings_field( 'carousel_background_color', __( 'Background color', 'jetpack' ), array( $this, 'carousel_background_color_callback' ), 'media', 'carousel_section' );
+ register_setting( 'media', 'carousel_background_color', array( $this, 'carousel_background_color_sanitize' ) );
+
+ add_settings_field( 'carousel_display_exif', __( 'Metadata', 'jetpack' ), array( $this, 'carousel_display_exif_callback' ), 'media', 'carousel_section' );
+ register_setting( 'media', 'carousel_display_exif', array( $this, 'carousel_display_exif_sanitize' ) );
+
+ // No geo setting yet, need to "fuzzify" data first, for privacy
+ // add_settings_field('carousel_display_geo', __( 'Geolocation', 'jetpack' ), array( $this, 'carousel_display_geo_callback' ), 'media', 'carousel_section' );
+ // register_setting( 'media', 'carousel_display_geo', array( $this, 'carousel_display_geo_sanitize' ) );
+ }
+
+ // Fulfill the settings section callback requirement by returning nothing
+ function carousel_section_callback() {
+ return;
+ }
+
+ function test_1or0_option( $value, $default_to_1 = true ) {
+ if ( true == $default_to_1 ) {
+ // Binary false (===) of $value means it has not yet been set, in which case we do want to default sites to 1
+ if ( false === $value ) {
+ $value = 1;
+ }
+ }
+ return ( 1 == $value ) ? 1 : 0;
+ }
+
+ function sanitize_1or0_option( $value ) {
+ return ( 1 == $value ) ? 1 : 0;
+ }
+
+ function settings_checkbox( $name, $label_text, $extra_text = '', $default_to_checked = true ) {
+ if ( empty( $name ) ) {
+ return;
+ }
+ $option = $this->test_1or0_option( get_option( $name ), $default_to_checked );
+ echo '<fieldset>';
+ echo '<input type="checkbox" name="' . esc_attr( $name ) . '" id="' . esc_attr( $name ) . '" value="1" ';
+ checked( '1', $option );
+ echo '/> <label for="' . esc_attr( $name ) . '">' . $label_text . '</label>';
+ if ( ! empty( $extra_text ) ) {
+ echo '<p class="description">' . $extra_text . '</p>';
+ }
+ echo '</fieldset>';
+ }
+
+ function settings_select( $name, $values, $extra_text = '' ) {
+ if ( empty( $name ) || ! is_array( $values ) || empty( $values ) ) {
+ return;
+ }
+ $option = get_option( $name );
+ echo '<fieldset>';
+ echo '<select name="' . esc_attr( $name ) . '" id="' . esc_attr( $name ) . '">';
+ foreach ( $values as $key => $value ) {
+ echo '<option value="' . esc_attr( $key ) . '" ';
+ selected( $key, $option );
+ echo '>' . esc_html( $value ) . '</option>';
+ }
+ echo '</select>';
+ if ( ! empty( $extra_text ) ) {
+ echo '<p class="description">' . $extra_text . '</p>';
+ }
+ echo '</fieldset>';
+ }
+
+ function carousel_display_exif_callback() {
+ $this->settings_checkbox( 'carousel_display_exif', __( 'Show photo metadata (<a href="http://en.wikipedia.org/wiki/Exchangeable_image_file_format" rel="noopener noreferrer" target="_blank">Exif</a>) in carousel, when available.', 'jetpack' ) );
+ }
+
+ function carousel_display_exif_sanitize( $value ) {
+ return $this->sanitize_1or0_option( $value );
+ }
+
+ function carousel_display_geo_callback() {
+ $this->settings_checkbox( 'carousel_display_geo', __( 'Show map of photo location in carousel, when available.', 'jetpack' ) );
+ }
+
+ function carousel_display_geo_sanitize( $value ) {
+ return $this->sanitize_1or0_option( $value );
+ }
+
+ function carousel_background_color_callback() {
+ $this->settings_select(
+ 'carousel_background_color', array(
+ 'black' => __( 'Black', 'jetpack' ),
+ 'white' => __( 'White', 'jetpack' ),
+ )
+ );
+ }
+
+ function carousel_background_color_sanitize( $value ) {
+ return ( 'white' == $value ) ? 'white' : 'black';
+ }
+
+ function carousel_enable_it_callback() {
+ $this->settings_checkbox( 'carousel_enable_it', __( 'Display images in full-size carousel slideshow.', 'jetpack' ) );
+ }
+
+ function carousel_enable_it_sanitize( $value ) {
+ return $this->sanitize_1or0_option( $value );
+ }
+}
+
+new Jetpack_Carousel;
diff --git a/plugins/jetpack/modules/carousel/rtl/jetpack-carousel-rtl.css b/plugins/jetpack/modules/carousel/rtl/jetpack-carousel-rtl.css
new file mode 100644
index 00000000..c42d454b
--- /dev/null
+++ b/plugins/jetpack/modules/carousel/rtl/jetpack-carousel-rtl.css
@@ -0,0 +1,1130 @@
+/* This file was automatically generated on Jul 30 2015 22:37:09 */
+
+.jp-carousel-wrap * {
+ line-height:inherit; /* prevent declarations of line-height in the universal selector */
+}
+
+.jp-carousel-overlay {
+ background: #000;
+}
+
+div.jp-carousel-fadeaway {
+ background: -moz-linear-gradient(bottom, rgba(0,0,0,0.5), rgba(0,0,0,0));
+ background: -webkit-gradient(linear, right bottom, right top, from(rgba(0,0,0,0.5)), to(rgba(0,0,0,0)));
+ position: fixed;
+ bottom: 0;
+ z-index: 2147483647;
+ width: 100%;
+ height: 15px;
+}
+
+.jp-carousel-next-button span,
+.jp-carousel-previous-button span {
+ background: url(.././images/arrows.png) no-repeat center center;
+ background-size: 200px 126px;
+}
+
+.jp-carousel-msg {
+ font-family: "Open Sans", sans-serif;
+ font-style: normal;
+ display: inline-block;
+ line-height: 19px;
+ padding: 11px 15px;
+ font-size: 14px;
+ text-align: center;
+ margin: 25px 2px 0 20px;
+ background-color: #fff;
+ border-right: 4px solid #ffba00;
+ -webkit-box-shadow: 0 0 1px 1px rgba(0,0,0,0.1);
+ box-shadow: 0 0 1px 1px rgba(0,0,0,0.1);
+}
+
+@media
+only screen and (-webkit-min-device-pixel-ratio: 1.5),
+only screen and (-o-min-device-pixel-ratio: 3/2),
+only screen and (min--moz-device-pixel-ratio: 1.5),
+only screen and (min-device-pixel-ratio: 1.5) {
+ .jp-carousel-next-button span,
+ .jp-carousel-previous-button span {
+ background-image: url(.././images/arrows-2x.png);
+ }
+}
+
+.jp-carousel-wrap {
+ font-family: "Helvetica Neue", sans-serif !important;
+}
+
+.jp-carousel-info {
+ position: absolute;
+ bottom: 0;
+ text-align: right !important;
+ -webkit-font-smoothing: subpixel-antialiased !important;
+}
+
+.jp-carousel-info ::selection {
+ background: #68c9e8; /* Safari */
+ color: #fff;
+ }
+
+.jp-carousel-info ::-moz-selection {
+ background: #68c9e8; /* Firefox */
+ color: #fff;
+}
+
+.jp-carousel-photo-info {
+ position: relative;
+ right: 25%;
+ width: 50%;
+}
+
+.jp-carousel-transitions .jp-carousel-photo-info {
+ -webkit-transition: 400ms ease-out;
+ -moz-transition: 400ms ease-out;
+ -o-transition: 400ms ease-out;
+ transition: 400ms ease-out;
+}
+
+.jp-carousel-info h2 {
+ background: none !important;
+ border: none !important;
+ color: #999;
+ display: block !important;
+ font: normal 13px/1.25em "Helvetica Neue", sans-serif !important;
+ letter-spacing: 0 !important;
+ margin: 7px 0 0 0 !important;
+ padding: 10px 0 0 !important;
+ overflow: hidden;
+ text-align: right;
+ text-shadow: none !important;
+ text-transform: none !important;
+ -webkit-font-smoothing: subpixel-antialiased;
+}
+
+.jp-carousel-next-button,
+.jp-carousel-previous-button {
+ text-indent: -9999px;
+ overflow: hidden;
+ cursor: pointer;
+}
+
+.jp-carousel-next-button span,
+.jp-carousel-previous-button span {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 82px;
+ zoom: 1;
+ filter: alpha(opacity=20);
+ opacity: 0.2;
+}
+
+.jp-carousel-transitions .jp-carousel-next-button span,
+.jp-carousel-transitions .jp-carousel-previous-button span {
+ -webkit-transition: 500ms opacity ease-out;
+ -moz-transition: 500ms opacity ease-out;
+ -o-transition: 500ms opacity ease-out;
+ transition: 500ms opacity ease-out;
+}
+
+.jp-carousel-next-button:hover span,
+.jp-carousel-previous-button:hover span {
+ filter: alpha(opacity=60);
+ opacity: 0.6;
+}
+.jp-carousel-next-button span {
+ background-position: -110px center;
+ left: 0;
+}
+
+.jp-carousel-previous-button span {
+ background-position: -10px center;
+ right:0;
+}
+
+.jp-carousel-buttons {
+ margin:-18px -20px 15px;
+ padding:8px 10px;
+ border-bottom:1px solid #222;
+ background: #222;
+ text-align: center;
+}
+
+div.jp-carousel-buttons a {
+ border: none !important;
+ color: #999;
+ font: normal 11px/1.2em "Helvetica Neue", sans-serif !important;
+ letter-spacing: 0 !important;
+ padding: 5px 0 5px 2px;
+ text-decoration: none !important;
+ text-shadow: none !important;
+ vertical-align: middle;
+ -webkit-font-smoothing: subpixel-antialiased;
+}
+
+div.jp-carousel-buttons a:hover {
+ color: #68c9e8;
+ border: none !important;
+}
+
+.jp-carousel-transitions div.jp-carousel-buttons a:hover {
+ -webkit-transition: none !important;
+ -moz-transition: none !important;
+ -o-transition: none !important;
+ transition: none !important;
+}
+
+.jp-carousel-slide, .jp-carousel-slide img, .jp-carousel-next-button,
+.jp-carousel-previous-button {
+ -webkit-transform:translate3d(0, 0, 0);
+ -moz-transform:translate3d(0, 0, 0);
+ -o-transform:translate3d(0, 0, 0);
+ -ms-transform:translate3d(0, 0, 0);
+}
+
+.jp-carousel-slide {
+ position:fixed;
+ width:0;
+ bottom:0;
+ background-color:#000;
+ border-radius:2px;
+ -webkit-border-radius:2px;
+ -moz-border-radius:2px;
+ -ms-border-radius:2px;
+ -o-border-radius:2px;
+}
+
+.jp-carousel-transitions .jp-carousel-slide {
+ -webkit-transition: 300ms ease-out;
+ -moz-transition: 300ms ease-out;
+ -o-transition: 300ms ease-out;
+ transition: 300ms ease-out;
+}
+
+.jp-carousel-slide.selected {
+ position: absolute !important;
+ filter: alpha(opacity=100);
+ opacity: 1;
+}
+
+.jp-carousel-slide {
+ filter: alpha(opacity=25);
+ opacity: 0.25;
+}
+
+.jp-carousel-slide img {
+ display: block;
+ width: 100% !important;
+ height: 100% !important;
+ max-width: 100% !important;
+ max-height: 100% !important;
+ background: none !important;
+ border: none !important;
+ padding: 0 !important;
+ -webkit-box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ -moz-box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ zoom: 1;
+}
+
+.jp-carousel-transitions .jp-carousel-slide {
+ -webkit-transition: opacity 400ms linear;
+ -moz-transition: opacity 400ms linear;
+ -o-transition: opacity 400ms linear;
+ transition: opacity 400ms linear;
+}
+
+.jp-carousel-close-hint {
+ color: #999;
+ cursor: default;
+ letter-spacing: 0 !important;
+ padding:0.35em 0 0;
+ position: absolute;
+ text-align: right;
+ width: 90%;
+}
+
+.jp-carousel-transitions .jp-carousel-close-hint {
+ -webkit-transition: color 200ms linear;
+ -moz-transition: color 200ms linear;
+ -o-transition: color 200ms linear;
+ transition: color 200ms linear;
+}
+
+.jp-carousel-close-hint span {
+ cursor: pointer;
+ background-color: black;
+ background-color: rgba(0,0,0,0.8);
+ display: block;
+ height: 22px;
+ font: 400 24px/1 "Helvetica Neue", sans-serif !important;
+ line-height: 22px;
+ margin: 0 0.4em 0 0;
+ text-align: center;
+ vertical-align: middle;
+ width: 22px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.jp-carousel-transitions .jp-carousel-close-hint span {
+ -webkit-transition: border-color 200ms linear;
+ -moz-transition: border-color 200ms linear;
+ -o-transition: border-color 200ms linear;
+ transition: border-color 200ms linear;
+}
+
+.jp-carousel-close-hint:hover {
+ cursor: default;
+ color: #fff;
+}
+
+.jp-carousel-close-hint:hover span {
+ border-color: #fff;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog,
+div.jp-carousel-buttons a.jp-carousel-commentlink,
+a.jp-carousel-image-download {
+ background: url(.././images/carousel-sprite.png?5) no-repeat;
+ background-size: 16px 200px;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog,
+div.jp-carousel-buttons a.jp-carousel-commentlink {
+ margin: 0 0 0 14px !important;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog.reblogged {
+ background-color: #303030;
+ padding-left: 8px !important;
+ border-radius: 2px;
+ border-radius: 2px;
+ -webkit-border-radius:2px;
+ -moz-border-radius:2px;
+ -ms-border-radius:2px;
+ -o-border-radius:2px;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog.reblogged {
+ margin: 0 -12px 0 2px !important;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog,
+div.jp-carousel-buttons a.jp-carousel-reblog.reblogged:hover {
+ background-position: 6px -36px;
+ padding-left: auto !important;
+ padding-right: 26px !important;
+ color: #999;
+}
+
+div.jp-carousel-buttons a.jp-carousel-commentlink {
+ background-position: 0px -156px;
+ padding-right: 19px !important;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog.reblogged:hover {
+ cursor: default;
+}
+
+div.jp-carousel-buttons a.jp-carousel-reblog:hover {
+ background-position: 6px -56px;
+ color: #68c9e8;
+}
+
+@media
+only screen and (-webkit-min-device-pixel-ratio: 1.5),
+only screen and (-o-min-device-pixel-ratio: 3/2),
+only screen and (min--moz-device-pixel-ratio: 1.5),
+only screen and (min-device-pixel-ratio: 1.5) {
+ div.jp-carousel-buttons a.jp-carousel-reblog,
+ div.jp-carousel-buttons a.jp-carousel-commentlink,
+ a.jp-carousel-image-download {
+ background-image: url(.././images/carousel-sprite-2x.png?5);
+ }
+}
+
+/* reblog */
+div#carousel-reblog-box {
+ background: #222;
+ background: -moz-linear-gradient(bottom, #222, #333);
+ background: -webkit-gradient(linear, right bottom, right top, from(#222), to(#333));
+ padding: 3px 0 0;
+ display: none;
+ margin: 5px auto 0;
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+ border-radius: 2px;
+ -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.9);
+ -moz-box-shadow: 0 0 20px rgba(0,0,0,0.9);
+ box-shadow: 0 0 20px rgba(0,0,0,0.9);
+ height: 74px;
+ width: 565px;
+}
+
+#carousel-reblog-box textarea {
+ background: #999;
+ font: 13px/1.4 "Helvetica Neue", sans-serif !important;
+ color: #444;
+ padding: 3px 6px;
+ width: 370px;
+ height: 48px;
+ float: right;
+ margin: 6px 9px 0 9px;
+ border: 1px solid #666;
+ -webkit-box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+ border-radius: 2px;
+}
+
+#carousel-reblog-box textarea:focus {
+ background: #ccc;
+ color: #222;
+}
+
+#carousel-reblog-box label {
+ color: #aaa;
+ font-size: 11px;
+ padding-left: 2px;
+ padding-right: 2px;
+ display: inline;
+ font-weight: normal;
+}
+
+#carousel-reblog-box select {
+ width: 110px;
+ padding: 0;
+ font-size: 12px;
+ font-family: "Helvetica Neue", sans-serif !important;
+ background: #333;
+ color: #eee;
+ border: 1px solid #444;
+ margin-top:5px;
+}
+
+#carousel-reblog-box .submit,
+#wrapper #carousel-reblog-box p.response {
+ float: right;
+ width: 154px;
+ padding-top: 0;
+ padding-right: 1px;
+ overflow: hidden;
+ height: 34px;
+ margin:3px 2px 0 0 !important;
+}
+
+#wrapper #carousel-reblog-box p.response {
+ font-size: 13px;
+ clear: none;
+ padding-right: 2px;
+ height: 34px;
+ color: #aaa;
+}
+
+#carousel-reblog-box input#carousel-reblog-submit, #jp-carousel-comment-form-button-submit {
+ font: 13px/24px "Helvetica Neue", sans-serif !important;
+ margin-top: 8px;
+ padding: 0 10px !important;
+ border-radius: 1em;
+ height: 24px;
+ color: #333;
+ cursor:pointer;
+ font-weight: normal;
+ background: #aaa;
+ background: -moz-linear-gradient(bottom, #aaa, #ccc);
+ background: -webkit-gradient(linear, right bottom, right top, from(#aaa), to(#ccc));
+ border: 1px solid #444;
+}
+
+#carousel-reblog-box input#carousel-reblog-submit:hover, #jp-carousel-comment-form-button-submit:hover {
+ background: #ccc;
+ background: -moz-linear-gradient(bottom, #ccc, #eee);
+ background: -webkit-gradient(linear, right bottom, right top, from(#ccc), to(#eee));
+}
+
+#carousel-reblog-box .canceltext {
+ color: #aaa;
+ font-size: 11px;
+ line-height: 24px;
+}
+
+#carousel-reblog-box .canceltext a {
+ color: #fff;
+}
+/* reblog end */
+
+
+/** Title and Desc Start **/
+.jp-carousel-titleanddesc {
+ border-top: 1px solid #222;
+ color: #999;
+ font-size: 15px;
+ padding-top: 24px;
+ margin-bottom: 20px;
+ font-weight:400;
+}
+.jp-carousel-titleanddesc-title {
+ font: 300 1.5em/1.1 "Helvetica Neue", sans-serif !important;
+ text-transform: none !important; /* prevents uppercase from leaking through */
+ color: #fff;
+ margin: 0 0 15px;
+ padding:0;
+}
+
+.jp-carousel-titleanddesc-desc p {
+ color: #999;
+ line-height:1.4;
+ margin-bottom: 0.75em;
+}
+
+.jp-carousel-titleanddesc p a,
+.jp-carousel-comments p a,
+.jp-carousel-info h2 a {
+ color: #fff !important;
+ border: none !important;
+ text-decoration: underline !important;
+ font-weight: normal !important;
+ font-style: normal !important;
+}
+
+.jp-carousel-titleanddesc p strong,
+.jp-carousel-titleanddesc p b {
+ font-weight: bold;
+ color: #999;
+}
+
+.jp-carousel-titleanddesc p em,
+.jp-carousel-titleanddesc p i {
+ font-style: italic;
+ color: #999;
+}
+
+
+.jp-carousel-titleanddesc p a:hover,
+.jp-carousel-comments p a:hover,
+.jp-carousel-info h2 a:hover {
+ color: #68c9e8 !important;
+}
+
+.jp-carousel-titleanddesc p:empty {
+ display: none;
+}
+
+.jp-carousel-photo-info h1:before,
+.jp-carousel-photo-info h1:after,
+.jp-carousel-left-column-wrapper h1:before,
+.jp-carousel-left-column-wrapper h1:after {
+ content:none !important;
+}
+/** Title and Desc End **/
+
+/** Meta Box Start **/
+.jp-carousel-image-meta {
+ background: #111;
+ border: 1px solid #222;
+ color: #fff;
+ font-size: 13px;
+ font: 12px/1.4 "Helvetica Neue", sans-serif !important;
+ overflow: hidden;
+ padding: 18px 20px;
+ width: 209px !important;
+}
+
+.jp-carousel-image-meta li,
+.jp-carousel-image-meta h5 {
+ font-family: "Helvetica Neue", sans-serif !important;
+ position: inherit !important;
+ top: auto !important;
+ left: auto !important;
+ right: auto !important;
+ bottom: auto !important;
+ background: none !important;
+ border: none !important;
+ font-weight: 400 !important;
+ line-height: 1.3em !important;
+}
+
+.jp-carousel-image-meta ul {
+ margin: 0 !important;
+ padding: 0 !important;
+ list-style: none !important;
+}
+
+.jp-carousel-image-meta li {
+ width: 48% !important;
+ float: right !important;
+ margin: 0 0 15px 2% !important;
+ color: #fff !important;
+ font-size:13px !important;
+}
+
+.jp-carousel-image-meta h5 {
+ color: #999 !important;
+ text-transform: uppercase !important;
+ font-size:10px !important;
+ margin:0 0 2px !important;
+ letter-spacing: 0.1em !important;
+}
+
+a.jp-carousel-image-download {
+ padding-right: 23px;
+ display: inline-block;
+ clear: both;
+ color: #999;
+ line-height: 1;
+ font-weight: 400;
+ font-size: 13px;
+ text-decoration: none;
+ background-position: 0 -82px;
+}
+
+a.jp-carousel-image-download span.photo-size {
+ font-size: 11px;
+ border-radius: 1em;
+ margin-right: 2px;
+ display: inline-block;
+}
+
+a.jp-carousel-image-download span.photo-size-times {
+ padding: 0 2px 0 1px;
+}
+
+a.jp-carousel-image-download:hover {
+ background-position: 0 -122px;
+ color: #68c9e8;
+ border: none !important;
+}
+
+/** Meta Box End **/
+
+/** GPS Map Start **/
+.jp-carousel-image-map {
+ position: relative;
+ margin: -20px -20px 20px;
+ border-bottom: 1px solid rgba( 255, 255, 255, 0.17 );
+ height: 154px;
+}
+
+.jp-carousel-image-map img.gmap-main {
+ -moz-border-radius-topleft: 6px;
+ border-top-right-radius: 6px;
+ border-left: 1px solid rgba( 255, 255, 255, 0.17 );
+}
+.jp-carousel-image-map div.gmap-topright {
+ width: 94px;
+ height: 154px;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+.jp-carousel-image-map div.imgclip {
+ overflow: hidden;
+ -moz-border-radius-topright: 6px;
+ border-top-left-radius: 6px;
+}
+.jp-carousel-image-map div.gmap-topright img {
+ margin-right: -40px;
+}
+.jp-carousel-image-map img.gmap-bottomright {
+ position: absolute;
+ top: 96px;
+ left: 0;
+}
+
+/** Comments Start **/
+.jp-carousel-comments {
+ font: 15px/1.7 "Helvetica Neue", sans-serif !important;
+ font-weight: 400;
+ background:none transparent;
+}
+
+.jp-carousel-comments p a:hover, .jp-carousel-comments p a:focus, .jp-carousel-comments p a:active {
+ color: #68c9e8 !important;
+}
+
+.jp-carousel-comment {
+ background:none transparent;
+ color: #999;
+ margin-bottom: 20px;
+ clear:right;
+ overflow: auto;
+ width: 100%
+}
+
+.jp-carousel-comment p {
+ color: #999 !important;
+}
+
+.jp-carousel-comment .comment-author {
+ font-size: 13px;
+ font-weight:400;
+ padding:0;
+ width:auto;
+ display: inline;
+ float:none;
+ border:none;
+ margin:0;
+}
+
+.jp-carousel-comment .comment-author a {
+ color: #fff;
+}
+
+.jp-carousel-comment .comment-gravatar {
+ float:right;
+}
+
+.jp-carousel-comment .comment-content {
+ border:none;
+ margin-right:85px;
+ padding: 0;
+}
+
+.jp-carousel-comment .avatar {
+ margin:0 0 0 20px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ border: none !important;
+ padding: 0 !important;
+ background-color: transparent !important;
+}
+
+.jp-carousel-comment .comment-date {
+ color:#999;
+ margin-top: 4px;
+ font-size:11px;
+ display: inline;
+ float: left;
+ /*clear: right;*/
+}
+
+#jp-carousel-comment-form {
+ margin:0 0 10px !important;
+ float: right;
+ width: 100%;
+}
+
+textarea#jp-carousel-comment-form-comment-field {
+ background: rgba(34,34,34,0.9);
+ border: 1px solid #3a3a3a;
+ color: #aaa;
+ font: 15px/1.4 "Helvetica Neue", sans-serif !important;
+ width: 100%;
+ padding: 10px 10px 5px;
+ margin: 0;
+ float: none;
+ height: 147px;
+ -webkit-box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ overflow: hidden;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+textarea#jp-carousel-comment-form-comment-field::-webkit-input-placeholder {
+ color: #555;
+}
+
+textarea#jp-carousel-comment-form-comment-field:focus {
+ background: #ccc;
+ color: #222;
+}
+
+textarea#jp-carousel-comment-form-comment-field:focus::-webkit-input-placeholder {
+ color: #aaa;
+}
+
+#jp-carousel-comment-form-spinner {
+ color: #fff;
+ margin:22px 10px 0 0;
+ display: block;
+ width: 20px;
+ height: 20px;
+ float: right;
+}
+
+#jp-carousel-comment-form-submit-and-info-wrapper {
+ display: none;
+ /*margin-bottom:15px;*/
+ overflow: hidden;
+ width: 100%
+}
+
+#jp-carousel-comment-form-commenting-as {
+}
+
+#jp-carousel-comment-form-commenting-as input {
+ background: rgba(34,34,34,0.9);
+ border: 1px solid #3a3a3a;
+ color: #aaa;
+ font: 13px/1.4 "Helvetica Neue", sans-serif !important;
+ padding: 3px 6px;
+ float: right;
+ -webkit-box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+ border-radius: 2px;
+ width:285px;
+}
+
+#jp-carousel-comment-form-commenting-as input:focus {
+ background: #ccc;
+ color: #222;
+}
+
+#jp-carousel-comment-form-commenting-as p {
+ font: 400 13px/1.7 "Helvetica Neue", sans-serif !important;
+ margin:22px 0 0;
+ float: right;
+}
+
+#jp-carousel-comment-form-commenting-as fieldset {
+ float:right;
+ border:none;
+ margin:20px 0 0 0;
+ padding:0;
+}
+
+#jp-carousel-comment-form-commenting-as fieldset {
+ clear: both;
+}
+
+#jp-carousel-comment-form-commenting-as label {
+ font: 400 13px/1.7 "Helvetica Neue", sans-serif !important;
+ margin:0 0 3px 20px;
+ float:right;
+ width:100px;
+}
+
+#jp-carousel-comment-form-button-submit {
+ margin-top: 20px;
+ float:left;
+}
+
+#js-carousel-comment-form-container {
+ margin-bottom:15px;
+ overflow: auto;
+ width: 100%;
+}
+
+#jp-carousel-comment-form-container {
+ margin-bottom:15px;
+ overflow: auto;
+ width: 100%;
+}
+
+#jp-carousel-comment-post-results {
+ display: none;
+ overflow:auto;
+ width:100%;
+}
+
+#jp-carousel-comment-post-results span {
+ display:block;
+ text-align: center;
+ margin-top:20px;
+ width: 100%;
+ overflow: auto;
+ padding: 1em 0;
+ box-sizing: border-box;
+ background: rgba( 0, 0, 0, 0.7 );
+ border-radius: 2px;
+ font: 13px/1.4 "Helvetica Neue", sans-serif !important;
+ border: 1px solid rgba( 255, 255, 255, 0.17 );
+ -webkit-box-shadow: inset 0px 5px 5px 0px rgba(0, 0, 0, 1);
+ box-shadow: inset 0px 5px 5px 0px rgba(0, 0, 0, 1);
+}
+
+.jp-carousel-comment-post-error {
+ color:#DF4926;
+}
+
+.jp-carousel-comment-post-success {
+ /*color:#21759B;*/
+}
+
+#jp-carousel-comments-closed {
+ display: none;
+ color: #999;
+}
+
+#jp-carousel-comments-loading {
+ font: 400 15px/1.7 "Helvetica Neue", sans-serif !important;
+ display: none;
+ color: #999;
+ text-align: right;
+ margin-bottom: 20px;
+}
+
+
+/* ----- Light variant ----- */
+
+.jp-carousel-light .jp-carousel-overlay {
+ background: #fff;
+}
+
+.jp-carousel-light .jp-carousel-next-button:hover span,
+.jp-carousel-light .jp-carousel-previous-button:hover span {
+ opacity: 0.8;
+}
+
+.jp-carousel-light .jp-carousel-close-hint:hover,
+.jp-carousel-light .jp-carousel-titleanddesc div {
+ color: #000 !important;
+}
+
+.jp-carousel-light .jp-carousel-comments p a,
+.jp-carousel-light .jp-carousel-comment .comment-author a,
+.jp-carousel-light .jp-carousel-titleanddesc p a,
+.jp-carousel-light .jp-carousel-titleanddesc p a,
+.jp-carousel-light .jp-carousel-comments p a,
+.jp-carousel-light .jp-carousel-info h2 a {
+ color: #1e8cbe !important;
+}
+
+.jp-carousel-light .jp-carousel-comments p a:hover,
+.jp-carousel-light .jp-carousel-comment .comment-author a:hover,
+.jp-carousel-light .jp-carousel-titleanddesc p a:hover,
+.jp-carousel-light .jp-carousel-titleanddesc p a:hover,
+.jp-carousel-light .jp-carousel-comments p a:hover,
+.jp-carousel-light .jp-carousel-info h2 a:hover {
+ color: #f1831e !important;
+}
+
+.jp-carousel-light .jp-carousel-info h2,
+.jp-carousel-light .jp-carousel-titleanddesc,
+.jp-carousel-light .jp-carousel-titleanddesc p,
+.jp-carousel-light .jp-carousel-comment,
+.jp-carousel-light .jp-carousel-comment p,
+.jp-carousel-light div.jp-carousel-buttons a,
+.jp-carousel-light .jp-carousel-titleanddesc p strong,
+.jp-carousel-light .jp-carousel-titleanddesc p b,
+.jp-carousel-light .jp-carousel-titleanddesc p em,
+.jp-carousel-light .jp-carousel-titleanddesc p i {
+ color: #666;
+}
+
+.jp-carousel-light .jp-carousel-buttons {
+ border-bottom-color: #f0f0f0;
+ background: #f5f5f5;
+}
+
+.jp-carousel-light div.jp-carousel-buttons a:hover {
+ text-decoration: none;
+ color: #f1831e;
+}
+
+.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog,
+.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog:hover {
+ background-position: 4px -56px;
+ padding-right: 24px !important;
+}
+
+.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog.reblogged {
+ background-color: #2ea2cc;
+ color: #fff;
+}
+
+.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-commentlink {
+ background-position: 0px -176px;
+}
+
+.jp-carousel-light div.jp-carousel-buttons a.jp-carousel-reblog.reblogged {
+ background-position: 5px -36px;
+}
+
+.jp-carousel-light div#carousel-reblog-box {
+ background: #eee;
+ background: -moz-linear-gradient(bottom, #ececec, #f7f7f7);
+ background: -webkit-gradient(linear, right bottom, right top, from(#ececec), to(#f7f7f7));
+ -webkit-box-shadow: 0 2px 6px rgba(0,0,0,0.1);
+ -moz-box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ border:1px solid #ddd;
+}
+
+.jp-carousel-light #carousel-reblog-box textarea {
+ border: 1px inset #ccc;
+ color: #666;
+ border: 1px solid #cfcfcf;
+ background: #fff;
+}
+
+.jp-carousel-light #carousel-reblog-box .canceltext {
+ color: #888;
+}
+
+.jp-carousel-light #carousel-reblog-box .canceltext a {
+ color: #666;
+}
+
+.jp-carousel-light #carousel-reblog-box select {
+ background: #eee;
+ color: #333;
+ border: 1px solid #aaa;
+}
+
+.jp-carousel-light #carousel-reblog-box input#carousel-reblog-submit, #jp-carousel-comment-form-button-submit {
+ color: #333;
+ background: #fff;
+ background: -moz-linear-gradient(bottom, #ddd, #fff);
+ background: -webkit-gradient(linear, right bottom, right top, from(#ddd), to(#fff));
+ border: 1px solid #aaa;
+}
+
+.jp-carousel-light .jp-carousel-image-meta {
+ background: #fafafa;
+ border: 1px solid #eee;
+ border-top-color: #f5f5f5;
+ border-right-color: #f5f5f5;
+ color: #333;
+}
+
+.jp-carousel-light .jp-carousel-image-meta li {
+ color: #000 !important;
+}
+
+.jp-carousel-light .jp-carousel-close-hint {
+ color: #ccc;
+}
+
+.jp-carousel-light .jp-carousel-close-hint span {
+ background-color: white;
+ border-color: #ccc;
+}
+
+.jp-carousel-light #jp-carousel-comment-form-comment-field::-webkit-input-placeholder {
+ color: #aaa;
+}
+
+.jp-carousel-light #jp-carousel-comment-form-comment-field:focus {
+ color: #333;
+}
+
+.jp-carousel-light #jp-carousel-comment-form-comment-field:focus::-webkit-input-placeholder {
+ color: #ddd;
+}
+
+.jp-carousel-light a.jp-carousel-image-download {
+ background-position: 0 -122px;
+}
+
+.jp-carousel-light a.jp-carousel-image-download:hover {
+ background-position: 0 -122px;
+ color: #f1831e;
+}
+
+.jp-carousel-light textarea#jp-carousel-comment-form-comment-field {
+ background: #fbfbfb;
+ color: #333;
+ border: 1px solid #dfdfdf;
+ -webkit-box-shadow: inset 2px 2px 2px rgba(0,0,0,0.1);
+ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.1);
+}
+
+.jp-carousel-light #jp-carousel-comment-form-commenting-as input {
+ background: #fbfbfb;
+ border: 1px solid #dfdfdf;
+ color: #333;
+ -webkit-box-shadow: inset 2px 2px 2px rgba(0,0,0,0.1);
+ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.1);
+}
+
+.jp-carousel-light #jp-carousel-comment-form-commenting-as input:focus {
+ background: #fbfbfb;
+ color: #333;
+}
+
+.jp-carousel-light #jp-carousel-comment-post-results span {
+ background: #f7f7f7;
+ border:1px solid #dfdfdf;
+ -webkit-box-shadow: inset 0px 0px 5px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0px 0px 5px rgba(0, 0, 0, 0.05);
+}
+
+.jp-carousel-light .jp-carousel-slide {
+ background-color:#fff;
+}
+
+.jp-carousel-light .jp-carousel-titleanddesc {
+ border-top: 1px solid #eee;
+}
+
+.jp-carousel-light .jp-carousel-fadeaway {
+ background: -moz-linear-gradient(bottom, rgba(255,255,255,0.75), rgba(255,255,255,0));
+ background: -webkit-gradient(linear, right bottom, right top, from(rgba(255,255,255,0.75)), to(rgba(255,255,255,0)));
+}
+
+/* Small screens */
+@media only screen and (max-width: 760px) {
+
+ .jp-carousel-info {
+ margin: 0 10px !important;
+ }
+
+ .jp-carousel-next-button, .jp-carousel-previous-button {
+ display: none !important;
+ }
+
+ .jp-carousel-buttons {
+ display: none !important;
+ }
+
+ .jp-carousel-image-meta {
+ float: none !important;
+ width: 100% !important;
+ -moz-box-sizing:border-box;
+ -webkit-box-sizing:border-box;
+ box-sizing: border-box;
+ }
+
+ .jp-carousel-close-hint {
+ font-weight: 800 !important;
+ font-size: 26px !important;
+ position: fixed !important;
+ top: -10px;
+ }
+
+ .jp-carousel-slide img {
+ filter: alpha(opacity=100);
+ opacity: 1;
+ }
+
+ .jp-carousel-wrap {
+ background-color: #000;
+ }
+
+ .jp-carousel-fadeaway {
+ display: none;
+ }
+
+ #jp-carousel-comment-form-container {
+ display: none !important;
+ }
+
+ .jp-carousel-titleanddesc {
+ padding-top: 0 !important;
+ border: none !important;
+ }
+ .jp-carousel-titleanddesc-title {
+ font-size: 1em !important;
+ }
+
+ .jp-carousel-left-column-wrapper {
+ padding: 0;
+ width: 100% !important;
+ }
+
+ .jp-carousel-photo-info {
+ right: 0 !important;
+ width: 100% !important;
+ }
+}
diff --git a/plugins/jetpack/modules/comment-likes.php b/plugins/jetpack/modules/comment-likes.php
new file mode 100644
index 00000000..4fd73751
--- /dev/null
+++ b/plugins/jetpack/modules/comment-likes.php
@@ -0,0 +1,205 @@
+<?php
+/**
+ * Module Name: Comment Likes
+ * Module Description: Increase visitor engagement by adding a Like button to comments.
+ * Sort Order: 39
+ * Recommendation Order: 17
+ * First Introduced: 5.1
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Social
+ * Additional Search Queries: like widget, like button, like, likes
+ */
+
+Jetpack::dns_prefetch(
+ array(
+ '//widgets.wp.com',
+ )
+);
+
+require_once dirname( __FILE__ ) . '/likes/jetpack-likes-master-iframe.php';
+require_once dirname( __FILE__ ) . '/likes/jetpack-likes-settings.php';
+
+class Jetpack_Comment_Likes {
+ public static function init() {
+ static $instance = NULL;
+
+ if ( ! $instance ) {
+ $instance = new Jetpack_Comment_Likes;
+ }
+
+ return $instance;
+ }
+
+ private function __construct() {
+ $this->settings = new Jetpack_Likes_Settings();
+ $this->blog_id = Jetpack_Options::get_option( 'id' );
+ $this->url = home_url();
+ $this->url_parts = parse_url( $this->url );
+ $this->domain = $this->url_parts['host'];
+
+ add_action( 'template_redirect', array( $this, 'frontend_init' ) );
+ add_action( 'admin_init', array( $this, 'admin_init' ) );
+
+ if ( ! Jetpack::is_module_active( 'likes' ) ) {
+ $active = Jetpack::get_active_modules();
+
+ if ( ! in_array( 'sharedaddy', $active ) && ! in_array( 'publicize', $active ) ) {
+ // we don't have a sharing page yet
+ add_action( 'admin_menu', array( $this->settings, 'sharing_menu' ) );
+ }
+
+ if ( in_array( 'publicize', $active ) && ! in_array( 'sharedaddy', $active ) ) {
+ // we have a sharing page but not the global options area
+ add_action( 'pre_admin_screen_sharing', array( $this->settings, 'sharing_block' ), 20 );
+ add_action( 'pre_admin_screen_sharing', array( $this->settings, 'updated_message' ), -10 );
+ }
+
+ if( ! in_array( 'sharedaddy', $active ) ) {
+ add_action( 'admin_init', array( $this->settings, 'process_update_requests_if_sharedaddy_not_loaded' ) );
+ add_action( 'sharing_global_options', array( $this->settings, 'admin_settings_showbuttonon_init' ), 19 );
+ add_action( 'sharing_admin_update', array( $this->settings, 'admin_settings_showbuttonon_callback' ), 19 );
+ add_action( 'admin_init', array( $this->settings, 'add_meta_box' ) );
+ } else {
+ add_filter( 'sharing_meta_box_title', array( $this->settings, 'add_likes_to_sharing_meta_box_title' ) );
+ add_action( 'start_sharing_meta_box_content', array( $this->settings, 'meta_box_content' ) );
+ }
+
+ add_action( 'save_post', array( $this->settings, 'meta_box_save' ) );
+ add_action( 'edit_attachment', array( $this->settings, 'meta_box_save' ) );
+ add_action( 'sharing_global_options', array( $this->settings, 'admin_settings_init' ), 20 );
+ add_action( 'sharing_admin_update', array( $this->settings, 'admin_settings_callback' ), 20 );
+ }
+ }
+
+ public function admin_init() {
+ add_filter( 'manage_edit-comments_columns', array( $this, 'add_like_count_column' ) );
+ add_action( 'manage_comments_custom_column', array( $this, 'comment_likes_edit_column' ), 10, 2 );
+ add_action( 'admin_print_styles-edit-comments.php', array( $this, 'enqueue_admin_styles_scripts' ) );
+ }
+
+ public function comment_likes_edit_column( $column_name, $comment_id ) {
+ if ( 'comment_likes' !== $column_name ) {
+ return;
+ }
+
+ $permalink = get_permalink( get_the_ID() );
+ ?>
+ <a
+ data-comment-id="<?php echo absint( $comment_id ); ?>"
+ data-blog-id="<?php echo absint( $this->blog_id ); ?>"
+ class="comment-like-count"
+ id="comment-like-count-<?php echo absint( $comment_id ); ?>"
+ href="<?php echo esc_url( $permalink ); ?>#comment-<?php echo absint( $comment_id ); ?>"
+ >
+ <span class="like-count">0</span>
+ </a>
+ <?php
+ }
+
+ function enqueue_admin_styles_scripts() {
+ wp_enqueue_style( 'comment-like-count', plugins_url( 'comment-likes/admin-style.css', __FILE__ ), array(), JETPACK__VERSION );
+ wp_enqueue_script(
+ 'comment-like-count',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/comment-likes/comment-like-count.min.js',
+ 'modules/comment-likes/comment-like-count.js'
+ ),
+ array( 'jquery' ),
+ JETPACK__VERSION
+ );
+ }
+
+ public function add_like_count_column( $columns ) {
+ $columns['comment_likes'] = '<span class="vers"></span>';
+
+ return $columns;
+ }
+
+ public function frontend_init() {
+ if ( Jetpack_AMP_Support::is_amp_request() ) {
+ return;
+ }
+
+ add_action( 'wp_enqueue_scripts', array( $this, 'load_styles_register_scripts' ) );
+ add_filter( 'comment_text', array( $this, 'comment_likes' ), 10, 2 );
+ }
+
+ public function load_styles_register_scripts() {
+ if ( ! wp_style_is( 'open-sans', 'registered' ) ) {
+ wp_register_style( 'open-sans', 'https://fonts.googleapis.com/css?family=Open+Sans', array(), JETPACK__VERSION );
+ }
+ wp_enqueue_style( 'jetpack_likes', plugins_url( 'likes/style.css', __FILE__ ), array( 'open-sans' ), JETPACK__VERSION );
+ wp_enqueue_script(
+ 'postmessage',
+ Jetpack::get_file_url_for_environment( '_inc/build/postmessage.min.js', '_inc/postmessage.js' ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ false
+ );
+ wp_enqueue_script(
+ 'jetpack_resize',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/jquery.jetpack-resize.min.js',
+ '_inc/jquery.jetpack-resize.js'
+ ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ false
+ );
+ wp_enqueue_script( 'jetpack_likes_queuehandler', plugins_url( 'likes/queuehandler.js' , __FILE__ ), array( 'jquery', 'postmessage', 'jetpack_resize' ), JETPACK__VERSION, true );
+ }
+
+ public function comment_likes( $content, $comment = null ) {
+ if ( empty( $comment ) ) {
+ return $content;
+ }
+
+ if ( ! $this->settings->is_likes_visible() ) {
+ return $content;
+ }
+
+ $comment_id = get_comment_ID();
+ if ( empty( $comment_id ) && ! empty( $comment->comment_ID ) ) {
+ $comment_id = $comment->comment_ID;
+ }
+
+ if ( empty( $content ) || empty( $comment_id ) ) {
+ return $content;
+ }
+
+ // In case master iframe hasn't been loaded. This could be the case when Post Likes module is disabled,
+ // or on pages on which we have comments but post likes are disabled.
+ if ( false === has_action( 'wp_footer', 'jetpack_likes_master_iframe' ) ) {
+ add_action( 'wp_footer', 'jetpack_likes_master_iframe', 21 );
+ }
+
+ $uniqid = uniqid();
+
+ $src = sprintf( 'https://widgets.wp.com/likes/#blog_id=%1$d&amp;comment_id=%2$d&amp;origin=%3$s&amp;obj_id=%1$d-%2$d-%4$s', $this->blog_id, $comment_id, $this->domain, $uniqid );
+ $name = sprintf( 'like-comment-frame-%1$d-%2$d-%3$s', $this->blog_id, $comment_id, $uniqid );
+ $wrapper = sprintf( 'like-comment-wrapper-%1$d-%2$d-%3$s', $this->blog_id, $comment_id, $uniqid );
+
+ $html = '';
+ $html .= "<div class='jetpack-comment-likes-widget-wrapper jetpack-likes-widget-unloaded' id='$wrapper' data-src='$src' data-name='$name'>";
+ $html .= "<div class='likes-widget-placeholder comment-likes-widget-placeholder comment-likes'><span class='loading'>" . esc_html__( 'Loading...', 'jetpack' ) . "</span></div>";
+ $html .= "<div class='comment-likes-widget jetpack-likes-widget comment-likes'><span class='comment-like-feedback'></span>";
+ $html .= "<span class='sd-text-color'></span><a class='sd-link-color'></a>";
+ $html .= '</div></div>';
+
+ /**
+ * Filters the Comment Likes button content.
+ *
+ * @module comment-likes
+ *
+ * @since 5.1.0
+ *
+ * @param string $html Comment Likes button content.
+ */
+ $like_button = apply_filters( 'comment_like_button', $html );
+
+ return $content . $like_button;
+ }
+}
+
+Jetpack_Comment_Likes::init();
diff --git a/plugins/jetpack/modules/comment-likes/admin-style.css b/plugins/jetpack/modules/comment-likes/admin-style.css
new file mode 100644
index 00000000..7079fe7e
--- /dev/null
+++ b/plugins/jetpack/modules/comment-likes/admin-style.css
@@ -0,0 +1,45 @@
+.fixed .column-comment_likes {
+ width: 5.5em;
+ padding: 8px 0;
+ text-align: center;
+}
+
+.fixed .column-stats {
+ width: 5em;
+}
+
+.fixed .column-comment_likes .comment-like-count {
+ box-sizing: border-box;
+ display: inline-block;
+ padding: 0 8px;
+ height: 2em;
+ margin-top: 5px;
+ -webkit-border-radius: 5px;
+ border-radius: 5px;
+ background-color: #72777c;
+ color: #fff;
+ font-size: 11px;
+ line-height: 21px;
+}
+
+.fixed .column-comment_likes .comment-like-count:after {
+ border: none;
+}
+
+.fixed .column-comment_likes .comment-like-count:hover {
+ background-color: #0073aa;
+}
+
+.fixed .column-comment_likes .vers:before {
+ font: normal 20px/1 dashicons;
+ content: '\f155';
+ speak: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+@media screen and (max-width: 782px) {
+ .fixed .column-comment_likes {
+ display: none;
+ }
+}
diff --git a/plugins/jetpack/modules/comment-likes/comment-like-count.js b/plugins/jetpack/modules/comment-likes/comment-like-count.js
new file mode 100644
index 00000000..c46503b4
--- /dev/null
+++ b/plugins/jetpack/modules/comment-likes/comment-like-count.js
@@ -0,0 +1,42 @@
+jQuery( document ).ready( function( $ ) {
+ var jsonAPIbase = 'https://public-api.wordpress.com/rest/v1',
+ APIqueue = [];
+
+ function getCommentLikeCounts() {
+ $( '.comment-like-count' ).each( function() {
+ var blogId = $( this ).attr( 'data-blog-id' ),
+ commentId = $( this ).attr( 'data-comment-id' );
+
+ APIqueue.push( '/sites/' + blogId + '/comments/' + commentId + '/likes' );
+ } );
+
+ return $.ajax( {
+ type: 'GET',
+ url: jsonAPIbase + '/batch',
+ dataType: 'jsonp',
+ data: 'urls[]=' + APIqueue.map( encodeURIComponent ).join( '&urls[]=' ),
+ success: function( response ) {
+ for ( var path in response ) {
+ if ( ! response[ path ].error_data ) {
+ var urlPieces = path.split( '/' ),
+ commentId = urlPieces[ 4 ],
+ likeCount = response[ path ].found;
+
+ if ( likeCount < 1 ) {
+ return;
+ }
+
+ $( '#comment-like-count-' + commentId )
+ .find( '.like-count' )
+ .hide()
+ .text( likeCount )
+ .fadeIn();
+ }
+ }
+ },
+ error: function() {},
+ } );
+ }
+
+ getCommentLikeCounts();
+} );
diff --git a/plugins/jetpack/modules/comments.php b/plugins/jetpack/modules/comments.php
new file mode 100644
index 00000000..1dc4954d
--- /dev/null
+++ b/plugins/jetpack/modules/comments.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * Module Name: Comments
+ * Module Description: Let readers use WordPress.com, Twitter, Facebook, or Google+ accounts to comment
+ * First Introduced: 1.4
+ * Sort Order: 20
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Social
+ * Feature: Engagement
+ * Additional Search Queries: comments, comment, facebook, twitter, google+, social
+ */
+
+require dirname( __FILE__ ) . '/comments/comments.php';
+
+if ( is_admin() ) {
+ require dirname( __FILE__ ) . '/comments/admin.php';
+}
+
+function jetpack_comments_load() {
+ Jetpack::enable_module_configurable( __FILE__ );
+}
+
+add_action( 'jetpack_modules_loaded', 'jetpack_comments_load' );
+
+Jetpack::dns_prefetch( array(
+ '//jetpack.wordpress.com',
+ '//s0.wp.com',
+ '//s1.wp.com',
+ '//s2.wp.com',
+ '//public-api.wordpress.com',
+ '//0.gravatar.com',
+ '//1.gravatar.com',
+ '//2.gravatar.com',
+) );
diff --git a/plugins/jetpack/modules/comments/admin.php b/plugins/jetpack/modules/comments/admin.php
new file mode 100644
index 00000000..59300465
--- /dev/null
+++ b/plugins/jetpack/modules/comments/admin.php
@@ -0,0 +1,210 @@
+<?php
+
+class Jetpack_Comments_Settings {
+
+ /** Variables *************************************************************/
+
+ /**
+ * The Jetpack Coments singleton
+ */
+ public $jetpack_comments;
+
+ /**
+ * The default comment form greeting
+ * @var string
+ */
+ public $default_greeting = ''; // Set in constructor
+
+ /**
+ * The default comment form color scheme
+ * @var string
+ */
+ public $color_schemes = array();
+
+ public static function init() {
+ static $instance = false;
+
+ if ( ! $instance ) {
+ $instance = new Jetpack_Comments_Settings( Jetpack_Comments::init() );
+ }
+
+ return $instance;
+ }
+
+ public function __construct( Highlander_Comments_Base $jetpack_comments ) {
+ $this->jetpack_comments = $jetpack_comments;
+
+ // Setup settings
+ add_action( 'admin_init', array( $this, 'add_settings' ) );
+ $this->setup_globals();
+ }
+
+ /** Private Methods *******************************************************/
+
+ /**
+ * Set any global variables or class variables
+ * @since JetpackComments (1.4)
+ */
+ protected function setup_globals() {
+ // Default option values
+ $this->default_greeting = __( 'Leave a Reply', 'jetpack' );
+
+ // Possible color schemes
+ $this->color_schemes = array(
+ 'light' => __( 'Light', 'jetpack' ),
+ 'dark' => __( 'Dark', 'jetpack' ),
+ 'transparent' => __( 'Transparent', 'jetpack' ),
+ );
+ }
+
+ /** Settings **************************************************************/
+
+ /**
+ * Add the Jetpack settings to WordPress's discussions page
+ *
+ * @since JetpackComments (1.4)
+ */
+ public function add_settings() {
+
+ // Create the section
+ add_settings_section(
+ 'jetpack_comment_form',
+ __( 'Comments', 'jetpack' ),
+ array( $this, 'comment_form_settings_section' ),
+ 'discussion'
+ );
+
+ /** Clever Greeting ***************************************************/
+
+ add_settings_field(
+ 'highlander_comment_form_prompt',
+ __( 'Greeting Text', 'jetpack' ),
+ array( $this, 'comment_form_greeting_setting' ),
+ 'discussion',
+ 'jetpack_comment_form'
+ );
+
+ register_setting(
+ 'discussion',
+ 'highlander_comment_form_prompt',
+ array( $this, 'comment_form_greeting_sanitize' )
+ );
+
+ /** Color Scheme ******************************************************/
+
+ add_settings_field(
+ 'jetpack_comment_form_color_scheme',
+ __( 'Color Scheme', 'jetpack' ),
+ array( $this, 'comment_form_color_scheme_setting' ),
+ 'discussion',
+ 'jetpack_comment_form'
+ );
+
+ register_setting(
+ 'discussion',
+ 'jetpack_comment_form_color_scheme',
+ array( $this, 'comment_form_color_scheme_sanitize' )
+ );
+ }
+
+ /**
+ * Discussions setting section blurb
+ *
+ * @since JetpackComments (1.4)
+ */
+ public function comment_form_settings_section() {
+ ?>
+
+ <p id="jetpack-comments-settings"><?php _e( 'Adjust your Comments form with a clever greeting and color-scheme.', 'jetpack' ); ?></p>
+
+ <?php
+ }
+
+ /**
+ * Custom Comment Greeting Text
+ *
+ * @since JetpackComments (1.4)
+ */
+ public function comment_form_greeting_setting() {
+
+ // The greeting
+ $greeting = get_option( 'highlander_comment_form_prompt', $this->default_greeting );
+ ?>
+
+ <input type="text" name="highlander_comment_form_prompt" id="jetpack-comment-form-greeting" value="<?php echo esc_attr( $greeting ); ?>" class="regular-text">
+ <p class="description"><?php _e( 'A few catchy words to motivate your readers to comment', 'jetpack' ); ?></p>
+
+ <?php
+ }
+
+ /**
+ * Sanitize the clever comment greeting
+ *
+ * @since JetpackComments (1.4)
+ * @param type $val
+ * @return string
+ */
+ function comment_form_greeting_sanitize( $val ) {
+
+ // Delete if empty or the default
+ if ( empty( $val ) || ( $this->default_greeting == $val ) ) {
+ delete_option( 'highlander_comment_form_prompt' );
+ return false;
+ }
+
+ return wp_kses( $val, array() );
+ }
+
+ /**
+ * Color Scheme Setting
+ *
+ * @since JetpackComments (1.4)
+ */
+ public function comment_form_color_scheme_setting() {
+
+ // The color scheme
+ $scheme = get_option( 'jetpack_comment_form_color_scheme', $this->jetpack_comments->default_color_scheme );
+ ?>
+
+ <fieldset>
+ <legend class="screen-reader-text"><?php _e( 'Color Scheme', 'jetpack' ); ?></legend>
+
+ <?php foreach ( $this->color_schemes as $key => $label ) : ?>
+
+ <label>
+ <input type="radio" name="jetpack_comment_form_color_scheme" id="jetpack-comment-form-color-scheme" value="<?php echo $key; ?>" <?php checked( $scheme, $key ); ?>>
+ <?php echo $label; ?>
+ </label>
+ <br />
+
+ <?php endforeach; ?>
+
+ </fieldset>
+
+ <?php
+ }
+
+ /**
+ * Sanitize the color scheme
+ *
+ * @since JetpackComments (1.4)
+ * @param type $val
+ * @return string
+ */
+ public function comment_form_color_scheme_sanitize( $val ) {
+
+ // Delete the option if it's...
+ if (
+ empty( $val ) || ! in_array( $val, array_keys( $this->color_schemes ) ) // ... unknown
+ ||
+ $val == $this->jetpack_comments->default_color_scheme // ... or the default
+ ) {
+ delete_option( 'jetpack_comment_form_color_scheme' );
+ return false;
+ }
+
+ return $val;
+ }
+}
+
+Jetpack_Comments_Settings::init();
diff --git a/plugins/jetpack/modules/comments/base.php b/plugins/jetpack/modules/comments/base.php
new file mode 100644
index 00000000..ecbbf1c4
--- /dev/null
+++ b/plugins/jetpack/modules/comments/base.php
@@ -0,0 +1,308 @@
+<?php
+
+/**
+ * All the code shared between WP.com Highlander and Jetpack Highlander
+ */
+class Highlander_Comments_Base {
+ function __construct() {
+ $this->setup_globals();
+ $this->setup_actions();
+ $this->setup_filters();
+ }
+
+ /**
+ * Set any global variables or class variables
+ * @since JetpackComments (1.4)
+ */
+ protected function setup_globals() {}
+
+ /**
+ * Setup actions for methods in this class
+ * @since JetpackComments (1.4)
+ */
+ protected function setup_actions() {
+ // Before a comment is posted
+ add_action( 'pre_comment_on_post', array( $this, 'allow_logged_out_user_to_comment_as_external' ) );
+
+ // After a comment is posted
+ add_action( 'comment_post', array( $this, 'set_comment_cookies' ) );
+ }
+
+ /**
+ * Setup filters for methods in this class
+ * @since JetpackComments (1.4)
+ */
+ protected function setup_filters() {
+ add_filter( 'comments_array', array( $this, 'comments_array' ) );
+ add_filter( 'preprocess_comment', array( $this, 'allow_logged_in_user_to_comment_as_guest' ), 0 );
+ }
+
+ /**
+ * Is this a Highlander POST request?
+ * Optionally restrict to one or more credentials slug (facebook, twitter, ...)
+ *
+ * @param string Comment credentials slug
+ * @param ...
+ * @return false|string false if it's not a Highlander POST request. The matching credentials slug if it is.
+ */
+ function is_highlander_comment_post() {
+ if ( empty( $_POST['hc_post_as'] ) ) {
+ return false;
+ }
+
+ if ( func_num_args() ) {
+ foreach ( func_get_args() as $id_source ) {
+ if ( $id_source === $_POST['hc_post_as'] ) {
+ return $id_source;
+ }
+ }
+ return false;
+ }
+
+ return is_string( $_POST['hc_post_as'] ) && in_array( $_POST['hc_post_as'], $this->id_sources ) ? $_POST['hc_post_as'] : false;
+ }
+
+ /**
+ * Signs an array of scalars with the self-hosted blog's Jetpack Token
+ *
+ * @param array $parameters
+ * @param string $key
+ * @return string HMAC
+ */
+ static function sign_remote_comment_parameters( $parameters, $key ) {
+ unset(
+ $parameters['sig'], // Don't sign the signature
+ $parameters['replytocom'] // This parameter is unsigned - it changes dynamically as the comment form moves from parent comment to parent comment
+ );
+
+ ksort( $parameters );
+
+ $signing = array();
+ foreach ( $parameters as $k => $v ) {
+ if ( ! is_scalar( $v ) ) {
+ return new WP_Error( 'invalid_input', __( 'Invalid request', 'jetpack' ) );
+ }
+
+ $signing[] = "{$k}={$v}";
+ }
+
+ return hash_hmac( 'sha1', implode( ':', $signing ), $key );
+ }
+
+ /*
+ * After commenting as a guest while logged in, the user needs to see both:
+ *
+ * ( user_id = blah AND comment_approved = 0 )
+ * and
+ * ( comment_author_email = blah AND comment_approved = 0 )
+ *
+ * Core only does the first since the user is logged in.
+ *
+ * Add the second to the comments array.
+ */
+ function comments_array( $comments ) {
+ global $wpdb, $post;
+
+ $commenter = $this->get_current_commenter();
+
+ if ( ! $commenter['user_id'] ) {
+ return $comments;
+ }
+
+ if ( ! $commenter['comment_author'] ) {
+ return $comments;
+ }
+
+ $in_moderation_comments = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT * FROM `$wpdb->comments` WHERE `comment_post_ID` = %d AND `user_id` = 0 AND `comment_author` = %s AND `comment_author_email` = %s AND `comment_approved` = '0' ORDER BY `comment_date_gmt` /* Highlander_Comments_Base::comments_array() */",
+ $post->ID,
+ wp_specialchars_decode( $commenter['comment_author'], ENT_QUOTES ),
+ $commenter['comment_author_email']
+ )
+ );
+
+ if ( ! $in_moderation_comments ) {
+ return $comments;
+ }
+
+ // @todo ZOMG this is a bad idea
+ $comments = array_merge( $comments, $in_moderation_comments );
+ usort( $comments, array( $this, 'sort_comments_by_comment_date_gmt' ) );
+
+ return $comments;
+ }
+
+ /**
+ * Comment sort comparator: comment_date_gmt
+ *
+ * @since JetpackComments (1.4)
+ * @param object $a
+ * @param object $b
+ * @return int
+ */
+ public function sort_comments_by_comment_date_gmt( $a, $b ) {
+ if ( $a->comment_date_gmt == $b->comment_date_gmt ) {
+ return 0;
+ }
+
+ return $a->comment_date_gmt < $b->comment_date_gmt ? -1 : 1;
+ }
+
+ /**
+ * Get the current commenter's information from their cookie
+ *
+ * @since JetpackComments (1.4)
+ * @return array Commenters information from cookie
+ */
+ protected function get_current_commenter() {
+ // Defaults
+ $user_id = 0;
+ $comment_author = '';
+ $comment_author_email = '';
+ $comment_author_url = '';
+
+ if ( isset( $_COOKIE[ 'comment_author_' . COOKIEHASH ] ) ) {
+ $comment_author = $_COOKIE[ 'comment_author_' . COOKIEHASH ];
+ }
+
+ if ( isset( $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] ) ) {
+ $comment_author_email = $_COOKIE[ 'comment_author_email_' . COOKIEHASH ];
+ }
+
+ if ( isset( $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] ) ) {
+ $comment_author_url = $_COOKIE[ 'comment_author_url_' . COOKIEHASH ];
+ }
+
+ if ( is_user_logged_in() ) {
+ $user = wp_get_current_user();
+ $user_id = $user->ID;
+ }
+
+ return compact( 'comment_author', 'comment_author_email', 'comment_author_url', 'user_id' );
+ }
+
+ /**
+ * Allows a logged out user to leave a comment as a facebook or twitter credentialed user.
+ * Overrides WordPress' core comment_registration option to treat these commenters as "registered" (verified) users.
+ *
+ * @since JetpackComments (1.4)
+ * @return If no
+ */
+ function allow_logged_out_user_to_comment_as_external() {
+ if ( ! $this->is_highlander_comment_post( 'facebook', 'twitter', 'googleplus' ) ) {
+ return;
+ }
+
+ add_filter( 'pre_option_comment_registration', '__return_zero' );
+ }
+
+ /**
+ * Allow a logged in user to post as a guest, FB, or twitter credentialed request.
+ * Bypasses WordPress' core overrides that force a logged in user to comment as that user.
+ * Respects comment_registration option.
+ *
+ * @since JetpackComments (1.4)
+ * @param array $comment_data
+ * @return int
+ */
+ function allow_logged_in_user_to_comment_as_guest( $comment_data ) {
+ // Bail if user registration is allowed
+ if ( get_option( 'comment_registration' ) ) {
+ return $comment_data;
+ }
+
+ // Bail if user is not logged in or not a post request
+ if ( 'POST' != strtoupper( $_SERVER['REQUEST_METHOD'] ) || ! is_user_logged_in() ) {
+ return $comment_data;
+ }
+
+ // Bail if this is not a guest or external service credentialed request
+ if ( ! $this->is_highlander_comment_post( 'guest', 'facebook', 'twitter', 'googleplus' ) ) {
+ return $comment_data;
+ }
+
+ $user = wp_get_current_user();
+
+ foreach ( array(
+ 'comment_author' => 'display_name',
+ 'comment_author_email' => 'user_email',
+ 'comment_author_url' => 'user_url',
+ ) as $comment_field => $user_field ) {
+ if ( $comment_data[ $comment_field ] != addslashes( $user->$user_field ) ) {
+ return $comment_data; // some other plugin already did something funky
+ }
+ }
+
+ if ( get_option( 'require_name_email' ) ) {
+ if ( 6 > strlen( $_POST['email'] ) || empty( $_POST['author'] ) ) {
+ wp_die( __( 'Error: please fill the required fields (name, email).', 'jetpack' ) );
+ } elseif ( ! is_email( $_POST['email'] ) ) {
+ wp_die( __( 'Error: please enter a valid email address.', 'jetpack' ) );
+ }
+ }
+
+ $author_change = false;
+ foreach ( array(
+ 'comment_author' => 'author',
+ 'comment_author_email' => 'email',
+ 'comment_author_url' => 'url',
+ ) as $comment_field => $post_field ) {
+ if ( $comment_data[ $comment_field ] != $_POST[ $post_field ] && 'url' != $post_field ) {
+ $author_change = true;
+ }
+ $comment_data[ $comment_field ] = $_POST[ $post_field ];
+ }
+
+ // Mark as guest comment if name or email were changed
+ if ( $author_change ) {
+ $comment_data['user_id'] = $comment_data['user_ID'] = 0;
+ }
+
+ return $comment_data;
+ }
+
+ /**
+ * Set the comment cookies or bail if comment is invalid
+ *
+ * @since JetpackComments (1.4)
+ * @param type $comment_id
+ * @return If comment is invalid
+ */
+ public function set_comment_cookies( $comment_id ) {
+ // Get comment and bail if it's invalid somehow
+ $comment = get_comment( $comment_id );
+ if ( empty( $comment ) || is_wp_error( $comment ) ) {
+ return;
+ }
+
+ $id_source = $this->is_highlander_comment_post();
+ if ( empty( $id_source ) ) {
+ return;
+ }
+
+ // Set comment author cookies
+ // phpcs:ignore WordPress.WP.CapitalPDangit
+ if ( ( 'wordpress' != $id_source ) && is_user_logged_in() ) {
+ /** This filter is already documented in core/wp-includes/comment-functions.php */
+ $comment_cookie_lifetime = apply_filters( 'comment_cookie_lifetime', 30000000 );
+ setcookie( 'comment_author_' . COOKIEHASH, $comment->comment_author, time() + $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN );
+ setcookie( 'comment_author_email_' . COOKIEHASH, $comment->comment_author_email, time() + $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN );
+ setcookie( 'comment_author_url_' . COOKIEHASH, esc_url( $comment->comment_author_url ), time() + $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN );
+ }
+ }
+
+ /**
+ * Get an avatar from Photon
+ *
+ * @since JetpackComments (1.4)
+ * @param string $url
+ * @param int $size
+ * @return string
+ */
+ protected function photon_avatar( $url, $size ) {
+ $size = (int) $size;
+
+ return jetpack_photon_url( $url, array( 'resize' => "$size,$size" ) );
+ }
+}
diff --git a/plugins/jetpack/modules/comments/comments.php b/plugins/jetpack/modules/comments/comments.php
new file mode 100644
index 00000000..f80da0fb
--- /dev/null
+++ b/plugins/jetpack/modules/comments/comments.php
@@ -0,0 +1,626 @@
+<?php
+
+require dirname( __FILE__ ) . '/base.php';
+
+/**
+ * Main Comments class
+ *
+ * @package JetpackComments
+ * @version 1.4
+ * @since 1.4
+ */
+class Jetpack_Comments extends Highlander_Comments_Base {
+
+ /** Variables *************************************************************/
+
+ /**
+ * Possible comment form sources
+ * @var array
+ */
+ public $id_sources = array();
+
+ /**
+ * URL
+ * @var string
+ */
+ public $signed_url = '';
+
+ /**
+ * The default comment form color scheme
+ * @var string
+ * @see ::set_default_color_theme_based_on_theme_settings()
+ */
+ public $default_color_scheme = 'light';
+
+ /** Methods ***************************************************************/
+
+ public static function init() {
+ static $instance = false;
+
+ if ( ! $instance ) {
+ $instance = new Jetpack_Comments;
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Main constructor for Comments
+ *
+ * @since JetpackComments (1.4)
+ */
+ public function __construct() {
+ parent::__construct();
+
+ // Comments is loaded
+
+ /**
+ * Fires after the Jetpack_Comments object has been instantiated
+ *
+ * @module comments
+ *
+ * @since 1.4.0
+ *
+ * @param array $jetpack_comments_loaded First element in array of type Jetpack_Comments
+ **/
+ do_action_ref_array( 'jetpack_comments_loaded', array( $this ) );
+ add_action( 'after_setup_theme', array( $this, 'set_default_color_theme_based_on_theme_settings' ), 100 );
+ }
+
+ public function set_default_color_theme_based_on_theme_settings() {
+ if ( function_exists( 'twentyeleven_get_theme_options' ) ) {
+ $theme_options = twentyeleven_get_theme_options();
+ $theme_color_scheme = isset( $theme_options['color_scheme'] ) ? $theme_options['color_scheme'] : 'transparent';
+ } else {
+ $theme_color_scheme = get_theme_mod( 'color_scheme', 'transparent' );
+ }
+ // Default for $theme_color_scheme is 'transparent' just so it doesn't match 'light' or 'dark'
+ // The default for Jetpack's color scheme is still defined above as 'light'
+
+ if ( false !== stripos( $theme_color_scheme, 'light' ) ) {
+ $this->default_color_scheme = 'light';
+ } elseif ( false !== stripos( $theme_color_scheme, 'dark' ) ) {
+ $this->default_color_scheme = 'dark';
+ }
+ }
+
+ /** Private Methods *******************************************************/
+
+ /**
+ * Set any global variables or class variables
+ * @since JetpackComments (1.4)
+ */
+ protected function setup_globals() {
+ parent::setup_globals();
+
+ // Sources
+ $this->id_sources = array(
+ 'guest',
+ 'jetpack',
+ 'wordpress',
+ 'twitter',
+ 'facebook',
+ );
+ }
+
+ /**
+ * Setup actions for methods in this class
+ * @since JetpackComments (1.4)
+ */
+ protected function setup_actions() {
+ parent::setup_actions();
+
+ // Selfishly remove everything from the existing comment form
+ remove_all_actions( 'comment_form_before' );
+
+ // Selfishly add only our actions back to the comment form
+ add_action( 'comment_form_before', array( $this, 'comment_form_before' ) );
+ add_action( 'comment_form_after', array( $this, 'comment_form_after' ), 1 ); // Set very early since we remove everything outputed before our action.
+
+ // Before a comment is posted
+ add_action( 'pre_comment_on_post', array( $this, 'pre_comment_on_post' ), 1 );
+
+ // After a comment is posted
+ add_action( 'comment_post', array( $this, 'add_comment_meta' ) );
+ }
+
+ /**
+ * Setup filters for methods in this class
+ * @since 1.6.2
+ */
+ protected function setup_filters() {
+ parent::setup_filters();
+
+ add_filter( 'comment_post_redirect', array( $this, 'capture_comment_post_redirect_to_reload_parent_frame' ), 100 );
+ add_filter( 'get_avatar', array( $this, 'get_avatar' ), 10, 4 );
+ }
+
+ /**
+ * Get the comment avatar from Gravatar, Twitter, or Facebook
+ *
+ * @since JetpackComments (1.4)
+ *
+ * @param string $avatar Current avatar URL
+ * @param string $comment Comment for the avatar
+ * @param int $size Size of the avatar
+ * @param string $default Not used
+ *
+ * @return string New avatar
+ */
+ public function get_avatar( $avatar, $comment, $size, $default ) {
+ if ( ! isset( $comment->comment_post_ID ) || ! isset( $comment->comment_ID ) ) {
+ // it's not a comment - bail
+ return $avatar;
+ }
+
+ // Detect whether it's a Facebook or Twitter avatar
+ $foreign_avatar = get_comment_meta( $comment->comment_ID, 'hc_avatar', true );
+ $foreign_avatar_hostname = parse_url( $foreign_avatar, PHP_URL_HOST );
+ if ( ! $foreign_avatar_hostname ||
+ ! preg_match( '/\.?(graph\.facebook\.com|twimg\.com)$/', $foreign_avatar_hostname ) ) {
+ return $avatar;
+ }
+
+ // Return the FB or Twitter avatar
+ return preg_replace( '#src=([\'"])[^\'"]+\\1#', 'src=\\1' . esc_url( set_url_scheme( $this->photon_avatar( $foreign_avatar, $size ), 'https' ) ) . '\\1', $avatar );
+ }
+
+ /** Output Methods ********************************************************/
+
+ /**
+ * Start capturing the core comment_form() output
+ * @since JetpackComments (1.4)
+ */
+ public function comment_form_before() {
+ /**
+ * Filters the setting that determines if Jetpagk comments should be enabled for
+ * the current post type.
+ *
+ * @module comments
+ *
+ * @since 3.8.1
+ *
+ * @param boolean $return Should comments be enabled?
+ */
+ if ( ! apply_filters( 'jetpack_comment_form_enabled_for_' . get_post_type(), true ) ) {
+ return;
+ }
+
+ // Add some JS to the footer
+ add_action( 'wp_footer', array( $this, 'watch_comment_parent' ), 100 );
+
+ ob_start();
+ }
+
+ /**
+ * Noop the default comment form output, get some options, and output our
+ * tricked out totally radical comment form.
+ *
+ * @since JetpackComments (1.4)
+ */
+ public function comment_form_after() {
+ /** This filter is documented in modules/comments/comments.php */
+ if ( ! apply_filters( 'jetpack_comment_form_enabled_for_' . get_post_type(), true ) ) {
+ return;
+ }
+
+ // Throw it all out and drop in our replacement
+ ob_end_clean();
+
+ // If users are required to be logged in, and they're not, then we don't need to do anything else
+ if ( get_option( 'comment_registration' ) && ! is_user_logged_in() ) {
+ /**
+ * Changes the log in to comment prompt.
+ *
+ * @module comments
+ *
+ * @since 1.4.0
+ *
+ * @param string $var Default is "You must log in to post a comment."
+ */
+ echo '<p class="must-log-in">' . sprintf( apply_filters( 'jetpack_must_log_in_to_comment', __( 'You must <a href="%s">log in</a> to post a comment.', 'jetpack' ) ), wp_login_url( get_permalink() . '#respond' ) ) . '</p>';
+
+ return;
+ }
+
+ if ( in_array( 'subscriptions', Jetpack::get_active_modules() ) ) {
+ $stb_enabled = get_option( 'stb_enabled', 1 );
+ $stb_enabled = empty( $stb_enabled ) ? 0 : 1;
+
+ $stc_enabled = get_option( 'stc_enabled', 1 );
+ $stc_enabled = empty( $stc_enabled ) ? 0 : 1;
+ } else {
+ $stb_enabled = 0;
+ $stc_enabled = 0;
+ }
+
+ $params = array(
+ 'blogid' => Jetpack_Options::get_option( 'id' ),
+ 'postid' => get_the_ID(),
+ 'comment_registration' => ( get_option( 'comment_registration' ) ? '1' : '0' ), // Need to explicitly send a '1' or a '0' for these
+ 'require_name_email' => ( get_option( 'require_name_email' ) ? '1' : '0' ),
+ 'stc_enabled' => $stc_enabled,
+ 'stb_enabled' => $stb_enabled,
+ 'show_avatars' => ( get_option( 'show_avatars' ) ? '1' : '0' ),
+ 'avatar_default' => get_option( 'avatar_default' ),
+ 'greeting' => get_option( 'highlander_comment_form_prompt', __( 'Leave a Reply', 'jetpack' ) ),
+ /**
+ * Changes the comment form prompt.
+ *
+ * @module comments
+ *
+ * @since 2.3.0
+ *
+ * @param string $var Default is "Leave a Reply to %s."
+ */
+ 'greeting_reply' => apply_filters( 'jetpack_comment_form_prompt_reply', __( 'Leave a Reply to %s', 'jetpack' ) ),
+ 'color_scheme' => get_option( 'jetpack_comment_form_color_scheme', $this->default_color_scheme ),
+ 'lang' => get_locale(),
+ 'jetpack_version' => JETPACK__VERSION,
+ );
+
+ // Extra parameters for logged in user
+ if ( is_user_logged_in() ) {
+ $current_user = wp_get_current_user();
+ $params['hc_post_as'] = 'jetpack';
+ $params['hc_userid'] = $current_user->ID;
+ $params['hc_username'] = $current_user->display_name;
+ $params['hc_userurl'] = $current_user->user_url;
+ $params['hc_useremail'] = md5( strtolower( trim( $current_user->user_email ) ) );
+ if ( current_user_can( 'unfiltered_html' ) ) {
+ $params['_wp_unfiltered_html_comment'] = wp_create_nonce( 'unfiltered-html-comment_' . get_the_ID() );
+ }
+ } else {
+ $commenter = wp_get_current_commenter();
+ $params['show_cookie_consent'] = (int) has_action( 'set_comment_cookies', 'wp_set_comment_cookies' );
+ $params['has_cookie_consent'] = (int) ! empty( $commenter['comment_author_email'] );
+ }
+
+ $signature = Jetpack_Comments::sign_remote_comment_parameters( $params, Jetpack_Options::get_option( 'blog_token' ) );
+ if ( is_wp_error( $signature ) ) {
+ $signature = 'error';
+ }
+
+ $params['sig'] = $signature;
+ $url_origin = set_url_scheme( 'http://jetpack.wordpress.com' );
+ $url = "{$url_origin}/jetpack-comment/?" . http_build_query( $params );
+ $url = "{$url}#parent=" . urlencode( set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ) );
+ $this->signed_url = $url;
+ $height = $params['comment_registration'] || is_user_logged_in() ? '315' : '430'; // Iframe can be shorter if we're not allowing guest commenting
+ $transparent = ( $params['color_scheme'] == 'transparent' ) ? 'true' : 'false';
+
+ if ( isset( $_GET['replytocom'] ) ) {
+ $url .= '&replytocom=' . (int) $_GET['replytocom'];
+ }
+
+ /**
+ * Filter whether the comment title can be displayed.
+ *
+ * @module comments
+ *
+ * @since 4.7.0
+ *
+ * @param bool $show Can the comment be displayed? Default to true.
+ */
+ $show_greeting = apply_filters( 'jetpack_comment_form_display_greeting', true );
+
+ // The actual iframe (loads comment form from Jetpack server)
+ ?>
+
+ <div id="respond" class="comment-respond">
+ <?php if ( true === $show_greeting ) : ?>
+ <h3 id="reply-title" class="comment-reply-title"><?php comment_form_title( esc_html( $params['greeting'] ), esc_html( $params['greeting_reply'] ) ); ?>
+ <small><?php cancel_comment_reply_link( esc_html__( 'Cancel reply', 'jetpack' ) ); ?></small>
+ </h3>
+ <?php endif; ?>
+ <form id="commentform" class="comment-form">
+ <iframe title="<?php esc_attr_e( 'Comment Form', 'jetpack' ); ?>" src="<?php echo esc_url( $url ); ?>" style="width:100%; height: <?php echo $height; ?>px; border:0;" name="jetpack_remote_comment" class="jetpack_remote_comment" id="jetpack_remote_comment" sandbox="allow-same-origin allow-top-navigation allow-scripts allow-forms allow-popups"></iframe>
+ <?php if ( ! Jetpack_AMP_Support::is_amp_request() ) : ?>
+ <!--[if !IE]><!-->
+ <script>
+ document.addEventListener('DOMContentLoaded', function () {
+ var commentForms = document.getElementsByClassName('jetpack_remote_comment');
+ for (var i = 0; i < commentForms.length; i++) {
+ commentForms[i].allowTransparency = <?php echo $transparent; ?>;
+ commentForms[i].scrolling = 'no';
+ }
+ });
+ </script>
+ <!--<![endif]-->
+ <?php endif; ?>
+ </form>
+ </div>
+
+ <?php // Below is required for comment reply JS to work ?>
+
+ <input type="hidden" name="comment_parent" id="comment_parent" value="" />
+
+ <?php
+ }
+
+ /**
+ * Add some JS to wp_footer to watch for hierarchical reply parent change
+ *
+ * @since JetpackComments (1.4)
+ */
+ public function watch_comment_parent() {
+ $url_origin = set_url_scheme( 'http://jetpack.wordpress.com' );
+ ?>
+
+ <!--[if IE]>
+ <script type="text/javascript">
+ if ( 0 === window.location.hash.indexOf( '#comment-' ) ) {
+ // window.location.reload() doesn't respect the Hash in IE
+ window.location.hash = window.location.hash;
+ }
+ </script>
+ <![endif]-->
+ <script type="text/javascript">
+ (function () {
+ var comm_par_el = document.getElementById( 'comment_parent' ),
+ comm_par = ( comm_par_el && comm_par_el.value ) ? comm_par_el.value : '',
+ frame = document.getElementById( 'jetpack_remote_comment' ),
+ tellFrameNewParent;
+
+ tellFrameNewParent = function () {
+ if ( comm_par ) {
+ frame.src = "<?php echo esc_url_raw( $this->signed_url ); ?>" + '&replytocom=' + parseInt( comm_par, 10 ).toString();
+ } else {
+ frame.src = "<?php echo esc_url_raw( $this->signed_url ); ?>";
+ }
+ };
+
+ <?php if ( get_option( 'thread_comments' ) && get_option( 'thread_comments_depth' ) ) : ?>
+
+ if ( 'undefined' !== typeof addComment ) {
+ addComment._Jetpack_moveForm = addComment.moveForm;
+
+ addComment.moveForm = function ( commId, parentId, respondId, postId ) {
+ var returnValue = addComment._Jetpack_moveForm( commId, parentId, respondId, postId ),
+ cancelClick, cancel;
+
+ if ( false === returnValue ) {
+ cancel = document.getElementById( 'cancel-comment-reply-link' );
+ cancelClick = cancel.onclick;
+ cancel.onclick = function () {
+ var cancelReturn = cancelClick.call( this );
+ if ( false !== cancelReturn ) {
+ return cancelReturn;
+ }
+
+ if ( ! comm_par ) {
+ return cancelReturn;
+ }
+
+ comm_par = 0;
+
+ tellFrameNewParent();
+
+ return cancelReturn;
+ };
+ }
+
+ if ( comm_par == parentId ) {
+ return returnValue;
+ }
+
+ comm_par = parentId;
+
+ tellFrameNewParent();
+
+ return returnValue;
+ };
+ }
+
+ <?php endif; ?>
+
+ // Do the post message bit after the dom has loaded.
+ document.addEventListener( 'DOMContentLoaded', function () {
+ var iframe_url = <?php echo json_encode( esc_url_raw( $url_origin ) ); ?>;
+ if ( window.postMessage ) {
+ if ( document.addEventListener ) {
+ window.addEventListener( 'message', function ( event ) {
+ var origin = event.origin.replace( /^http:\/\//i, 'https://' );
+ if ( iframe_url.replace( /^http:\/\//i, 'https://' ) !== origin ) {
+ return;
+ }
+ jQuery( frame ).height( event.data );
+ });
+ } else if ( document.attachEvent ) {
+ window.attachEvent( 'message', function ( event ) {
+ var origin = event.origin.replace( /^http:\/\//i, 'https://' );
+ if ( iframe_url.replace( /^http:\/\//i, 'https://' ) !== origin ) {
+ return;
+ }
+ jQuery( frame ).height( event.data );
+ });
+ }
+ }
+ })
+
+ })();
+ </script>
+
+ <?php
+ }
+
+ /**
+ * Verify the hash included in remote comments.
+ *
+ * @since JetpackComments (1.4)
+ *
+ * @param type $comment Not used
+ */
+ public function pre_comment_on_post( $comment ) {
+ $post_array = stripslashes_deep( $_POST );
+
+ // Bail if missing the Jetpack token
+ if ( ! isset( $post_array['sig'] ) ) {
+ unset( $_POST['hc_post_as'] );
+
+ return;
+ }
+
+ if ( false !== strpos( $post_array['hc_avatar'], '.gravatar.com' ) ) {
+ $post_array['hc_avatar'] = htmlentities( $post_array['hc_avatar'] );
+ }
+
+ $check = Jetpack_Comments::sign_remote_comment_parameters( $post_array, Jetpack_Options::get_option( 'blog_token' ) );
+ if ( is_wp_error( $check ) ) {
+ wp_die( $check );
+ }
+
+ // Bail if token is expired or not valid
+ if ( $check !== $post_array['sig'] ) {
+ wp_die( __( 'Invalid security token.', 'jetpack' ) );
+ }
+
+ /** This filter is documented in modules/comments/comments.php */
+ if ( ! apply_filters( 'jetpack_comment_form_enabled_for_' . get_post_type( $post_array['comment_post_ID'] ), true ) ) {
+ // In case the comment POST is legit, but the comments are
+ // now disabled, we don't allow the comment
+
+ wp_die( __( 'Comments are not allowed.', 'jetpack' ) );
+ }
+ }
+
+ /** Capabilities **********************************************************/
+
+ /**
+ * Add some additional comment meta after comment is saved about what
+ * service the comment is from, the avatar, user_id, etc...
+ *
+ * @since JetpackComments (1.4)
+ *
+ * @param type $comment_id
+ */
+ public function add_comment_meta( $comment_id ) {
+ $comment_meta = array();
+
+ switch ( $this->is_highlander_comment_post() ) {
+ case 'facebook':
+ $comment_meta['hc_post_as'] = 'facebook';
+ $comment_meta['hc_avatar'] = stripslashes( $_POST['hc_avatar'] );
+ $comment_meta['hc_foreign_user_id'] = stripslashes( $_POST['hc_userid'] );
+ break;
+
+ case 'twitter':
+ $comment_meta['hc_post_as'] = 'twitter';
+ $comment_meta['hc_avatar'] = stripslashes( $_POST['hc_avatar'] );
+ $comment_meta['hc_foreign_user_id'] = stripslashes( $_POST['hc_userid'] );
+ break;
+
+ // phpcs:ignore WordPress.WP.CapitalPDangit
+ case 'wordpress':
+ // phpcs:ignore WordPress.WP.CapitalPDangit
+ $comment_meta['hc_post_as'] = 'wordpress';
+ $comment_meta['hc_avatar'] = stripslashes( $_POST['hc_avatar'] );
+ $comment_meta['hc_foreign_user_id'] = stripslashes( $_POST['hc_userid'] );
+ $comment_meta['hc_wpcom_id_sig'] = stripslashes( $_POST['hc_wpcom_id_sig'] ); //since 1.9
+ break;
+
+ case 'jetpack':
+ $comment_meta['hc_post_as'] = 'jetpack';
+ $comment_meta['hc_avatar'] = stripslashes( $_POST['hc_avatar'] );
+ $comment_meta['hc_foreign_user_id'] = stripslashes( $_POST['hc_userid'] );
+ break;
+
+ }
+
+ // Bail if no extra comment meta
+ if ( empty( $comment_meta ) ) {
+ return;
+ }
+
+ // Loop through extra meta and add values
+ foreach ( $comment_meta as $key => $value ) {
+ add_comment_meta( $comment_id, $key, $value, true );
+ }
+ }
+
+ function capture_comment_post_redirect_to_reload_parent_frame( $url ) {
+ if ( ! isset( $_GET['for'] ) || 'jetpack' != $_GET['for'] ) {
+ return $url;
+ }
+ ?>
+ <!DOCTYPE html>
+ <html <?php language_attributes(); ?>>
+ <!--<![endif]-->
+ <head>
+ <meta charset="<?php bloginfo( 'charset' ); ?>" />
+ <title><?php printf( __( 'Submitting Comment%s', 'jetpack' ), '&hellip;' ); ?></title>
+ <style type="text/css">
+ body {
+ display: table;
+ width: 100%;
+ height: 60%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+ color: #333;
+ }
+
+ h1 {
+ text-align: center;
+ margin: 0;
+ padding: 0;
+ display: table-cell;
+ vertical-align: middle;
+ font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", sans-serif;
+ font-weight: normal;
+ }
+
+ .hidden {
+ opacity: 0;
+ }
+
+ h1 span {
+ -moz-transition-property: opacity;
+ -moz-transition-duration: 1s;
+ -moz-transition-timing-function: ease-in-out;
+
+ -webkit-transition-property: opacity;
+ -webkit-transition-duration: 1s;
+ -webbit-transition-timing-function: ease-in-out;
+
+ -o-transition-property: opacity;
+ -o-transition-duration: 1s;
+ -o-transition-timing-function: ease-in-out;
+
+ -ms-transition-property: opacity;
+ -ms-transition-duration: 1s;
+ -ms-transition-timing-function: ease-in-out;
+
+ transition-property: opacity;
+ transition-duration: 1s;
+ transition-timing-function: ease-in-out;
+ }
+ </style>
+ </head>
+ <body>
+ <h1><?php printf( __( 'Submitting Comment%s', 'jetpack' ), '<span id="ellipsis" class="hidden">&hellip;</span>' ); ?></h1>
+ <script type="text/javascript">
+ try {
+ window.parent.location = <?php echo json_encode( $url ); ?>;
+ window.parent.location.reload(true);
+ } catch (e) {
+ window.location = <?php echo json_encode( $url ); ?>;
+ window.location.reload(true);
+ }
+ ellipsis = document.getElementById('ellipsis');
+
+ function toggleEllipsis() {
+ ellipsis.className = ellipsis.className ? '' : 'hidden';
+ }
+
+ setInterval(toggleEllipsis, 1200);
+ </script>
+ </body>
+ </html>
+ <?php
+ exit;
+ }
+}
+
+Jetpack_Comments::init();
diff --git a/plugins/jetpack/modules/contact-form.php b/plugins/jetpack/modules/contact-form.php
new file mode 100644
index 00000000..93c66df7
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Module Name: Contact Form
+ * Module Description: Add a customizable contact form to any post or page using the Jetpack Form Block.
+ * Sort Order: 15
+ * Recommendation Order: 14
+ * First Introduced: 1.3
+ * Requires Connection: No
+ * Auto Activate: Yes
+ * Module Tags: Other
+ * Feature: Writing
+ * Additional Search Queries: contact, form, grunion, feedback, submission, contact form, email, feedback, contact form plugin, custom form, custom form plugin, form builder, forms, form maker, survey, contact by jetpack, contact us, forms free
+ */
+
+require_once dirname( __FILE__ ) . '/contact-form/grunion-contact-form.php';
+/*
+ * Filters if the new Contact Form Editor View should be used.
+ *
+ * A temporary filter to disable the new Editor View for the older UI.
+ * Please note this filter and the old UI will be removed in the future.
+ * Expected to be removed in Jetpack 5.8 or if a security issue merits removing the old code sooner.
+ *
+ * @since 5.2.0
+ *
+ * @param boolean $view Use new Editor View. Default true.
+ */
+if ( is_admin() && apply_filters( 'tmp_grunion_allow_editor_view', true ) ) {
+ require_once dirname( __FILE__ ) . '/contact-form/grunion-editor-view.php';
+}
diff --git a/plugins/jetpack/modules/contact-form/admin.php b/plugins/jetpack/modules/contact-form/admin.php
new file mode 100644
index 00000000..0596f798
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/admin.php
@@ -0,0 +1,894 @@
+<?php
+/**
+ * Add a contact form button to the post composition screen
+ */
+add_action( 'media_buttons', 'grunion_media_button', 999 );
+function grunion_media_button() {
+ global $post_ID, $temp_ID, $pagenow;
+
+ if ( 'press-this.php' === $pagenow ) {
+ return;
+ }
+
+ $iframe_post_id = (int) ( 0 == $post_ID ? $temp_ID : $post_ID );
+ $title = __( 'Add Contact Form', 'jetpack' );
+ $plugin_url = esc_url( GRUNION_PLUGIN_URL );
+ $site_url = esc_url( admin_url( "/admin-ajax.php?post_id={$iframe_post_id}&action=grunion_form_builder&TB_iframe=true&width=768" ) );
+ ?>
+
+ <a id="insert-jetpack-contact-form" class="button thickbox" title="<?php echo esc_attr( $title ); ?>" data-editor="content" href="<?php echo $site_url; ?>&id=add_form">
+ <span class="jetpack-contact-form-icon"></span> <?php echo esc_html( $title ); ?>
+ </a>
+
+ <?php
+}
+
+add_action( 'wp_ajax_grunion_form_builder', 'grunion_display_form_view' );
+
+function grunion_display_form_view() {
+ if ( current_user_can( 'edit_posts' ) ) {
+ require_once GRUNION_PLUGIN_DIR . 'grunion-form-view.php';
+ }
+ exit;
+}
+
+// feedback specific css items
+add_action( 'admin_print_styles', 'grunion_admin_css' );
+function grunion_admin_css() {
+ global $current_screen;
+ if ( is_null( $current_screen ) ) {
+ return;
+ }
+ if ( 'edit-feedback' !== $current_screen->id ) {
+ return;
+ }
+
+ wp_enqueue_script( 'wp-lists' );
+?>
+
+<style type='text/css'>
+.add-new-h2, .view-switch, body.no-js .tablenav select[name^=action], body.no-js #doaction, body.no-js #doaction2 {
+ display: none
+}
+
+.column-feedback_from img {
+ float:left;
+ margin-right:10px;
+ margin-top:3px;
+}
+
+.widefat .column-feedback_from {
+ width: 17%;
+}
+.widefat .column-feedback_date {
+ width: 17%;
+}
+
+.spam a {
+ color: #BC0B0B;
+}
+
+.untrash a {
+ color: #D98500;
+}
+
+.unspam a {
+color: #D98500;
+}
+
+</style>
+
+<?php
+}
+
+/**
+ * Hack a 'Bulk Spam' option for bulk edit in other than spam view
+ * Hack a 'Bulk Delete' option for bulk edit in spam view
+ *
+ * There isn't a better way to do this until
+ * http://core.trac.wordpress.org/changeset/17297 is resolved
+ */
+add_action( 'admin_head', 'grunion_add_bulk_edit_option' );
+function grunion_add_bulk_edit_option() {
+
+ $screen = get_current_screen();
+
+ if ( is_null( $screen ) ) {
+ return;
+ }
+
+ if ( 'edit-feedback' != $screen->id ) {
+ return;
+ }
+
+ // When viewing spam we want to be able to be able to bulk delete
+ // When viewing anything we want to be able to bulk move to spam
+ if ( isset( $_GET['post_status'] ) && 'spam' == $_GET['post_status'] ) {
+ // Create Delete Permanently bulk item
+ $option_val = 'delete';
+ $option_txt = __( 'Delete Permanently', 'jetpack' );
+ $pseudo_selector = 'last-child';
+
+ } else {
+ // Create Mark Spam bulk item
+ $option_val = 'spam';
+ $option_txt = __( 'Mark as Spam', 'jetpack' );
+ $pseudo_selector = 'first-child';
+ }
+
+ ?>
+ <script type="text/javascript">
+ jQuery(document).ready(function($) {
+ $('#posts-filter .actions select').filter('[name=action], [name=action2]').find('option:<?php echo $pseudo_selector; ?>').after('<option value="<?php echo $option_val; ?>"><?php echo esc_attr( $option_txt ); ?></option>' );
+ })
+ </script>
+ <?php
+}
+
+/**
+ * Hack an 'Empty Spam' button to spam view
+ *
+ * Leverages core's delete_all functionality
+ */
+add_action( 'admin_head', 'grunion_add_empty_spam_button' );
+function grunion_add_empty_spam_button() {
+ $screen = get_current_screen();
+
+ if ( is_null( $screen ) ) {
+ return;
+ }
+
+ // Only add to feedback, only to spam view
+ if ( 'edit-feedback' != $screen->id
+ || empty( $_GET['post_status'] )
+ || 'spam' !== $_GET['post_status'] ) {
+ return;
+ }
+
+ // Get HTML for the button
+ $button_html = wp_nonce_field( 'bulk-destroy', '_destroy_nonce', true, false );
+ $button_html .= get_submit_button( __( 'Empty Spam', 'jetpack' ), 'apply', 'delete_all', false );
+
+ // Add the button next to the filter button via js
+ ?>
+ <script type="text/javascript">
+ jQuery(document).ready(function($) {
+ $('#posts-filter #post-query-submit').after('<?php echo $button_html; ?>' );
+ })
+ </script>
+ <?php
+}
+
+/**
+ * Handle a bulk spam report
+ */
+add_action( 'admin_init', 'grunion_handle_bulk_spam' );
+function grunion_handle_bulk_spam() {
+ global $pagenow;
+
+ if ( 'edit.php' != $pagenow
+ || ( empty( $_REQUEST['post_type'] ) || 'feedback' != $_REQUEST['post_type'] ) ) {
+ return;
+ }
+
+ // Slip in a success message
+ if ( ! empty( $_REQUEST['message'] ) && 'marked-spam' == $_REQUEST['message'] ) {
+ add_action( 'admin_notices', 'grunion_message_bulk_spam' );
+ }
+
+ if ( ( empty( $_REQUEST['action'] ) || 'spam' != $_REQUEST['action'] ) && ( empty( $_REQUEST['action2'] ) || 'spam' != $_REQUEST['action2'] ) ) {
+ return;
+ }
+
+ check_admin_referer( 'bulk-posts' );
+
+ if ( empty( $_REQUEST['post'] ) ) {
+ wp_safe_redirect( wp_get_referer() );
+ exit;
+ }
+
+ $post_ids = array_map( 'intval', $_REQUEST['post'] );
+
+ foreach ( $post_ids as $post_id ) {
+ if ( ! current_user_can( 'edit_page', $post_id ) ) {
+ wp_die( __( 'You are not allowed to manage this item.', 'jetpack' ) );
+ }
+
+ $post = array(
+ 'ID' => $post_id,
+ 'post_status' => 'spam',
+ );
+ $akismet_values = get_post_meta( $post_id, '_feedback_akismet_values', true );
+ wp_update_post( $post );
+
+ /**
+ * Fires after a comment has been marked by Akismet.
+ *
+ * Typically this means the comment is spam.
+ *
+ * @module contact-form
+ *
+ * @since 2.2.0
+ *
+ * @param string $comment_status Usually is 'spam', otherwise 'ham'.
+ * @param array $akismet_values From '_feedback_akismet_values' in comment meta
+ */
+ do_action( 'contact_form_akismet', 'spam', $akismet_values );
+ }
+
+ $redirect_url = add_query_arg( 'message', 'marked-spam', wp_get_referer() );
+ wp_safe_redirect( $redirect_url );
+ exit;
+}
+
+function grunion_message_bulk_spam() {
+ echo '<div class="updated"><p>' . __( 'Feedback(s) marked as spam', 'jetpack' ) . '</p></div>';
+}
+
+// remove admin UI parts that we don't support in feedback management
+add_action( 'admin_menu', 'grunion_admin_menu' );
+function grunion_admin_menu() {
+ global $menu, $submenu;
+ unset( $submenu['edit.php?post_type=feedback'] );
+}
+
+add_filter( 'bulk_actions-edit-feedback', 'grunion_admin_bulk_actions' );
+function grunion_admin_bulk_actions( $actions ) {
+ global $current_screen;
+ if ( 'edit-feedback' != $current_screen->id ) {
+ return $actions;
+ }
+
+ unset( $actions['edit'] );
+ return $actions;
+}
+
+add_filter( 'views_edit-feedback', 'grunion_admin_view_tabs' );
+function grunion_admin_view_tabs( $views ) {
+ global $current_screen;
+ if ( 'edit-feedback' != $current_screen->id ) {
+ return $views;
+ }
+
+ unset( $views['publish'] );
+
+ preg_match( '|post_type=feedback\'( class="current")?\>(.*)\<span class=|', $views['all'], $match );
+ if ( ! empty( $match[2] ) ) {
+ $views['all'] = str_replace( $match[2], __( 'Messages', 'jetpack' ) . ' ', $views['all'] );
+ }
+
+ return $views;
+}
+
+add_filter( 'manage_feedback_posts_columns', 'grunion_post_type_columns_filter' );
+function grunion_post_type_columns_filter( $cols ) {
+ $cols = array(
+ 'cb' => '<input type="checkbox" />',
+ 'feedback_from' => __( 'From', 'jetpack' ),
+ 'feedback_message' => __( 'Message', 'jetpack' ),
+ 'feedback_date' => __( 'Date', 'jetpack' ),
+ );
+
+ return $cols;
+}
+
+add_action( 'manage_posts_custom_column', 'grunion_manage_post_columns', 10, 2 );
+function grunion_manage_post_columns( $col, $post_id ) {
+ global $post;
+
+ /**
+ * Only call parse_fields_from_content if we're dealing with a Grunion custom column.
+ */
+ if ( ! in_array( $col, array( 'feedback_date', 'feedback_from', 'feedback_message' ) ) ) {
+ return;
+ }
+
+ $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $post_id );
+
+ switch ( $col ) {
+ case 'feedback_from':
+ $author_name = isset( $content_fields['_feedback_author'] ) ? $content_fields['_feedback_author'] : '';
+ $author_email = isset( $content_fields['_feedback_author_email'] ) ? $content_fields['_feedback_author_email'] : '';
+ $author_url = isset( $content_fields['_feedback_author_url'] ) ? $content_fields['_feedback_author_url'] : '';
+ $author_ip = isset( $content_fields['_feedback_ip'] ) ? $content_fields['_feedback_ip'] : '';
+ $form_url = isset( $post->post_parent ) ? get_permalink( $post->post_parent ) : null;
+
+ $author_name_line = '';
+ if ( ! empty( $author_name ) ) {
+ if ( ! empty( $author_email ) ) {
+ $author_name_line = get_avatar( $author_email, 32 );
+ }
+
+ $author_name_line .= sprintf( '<strong>%s</strong><br />', esc_html( $author_name ) );
+ }
+
+ $author_email_line = '';
+ if ( ! empty( $author_email ) ) {
+ $author_email_line = sprintf( "<a href='%1\$s' target='_blank'>%2\$s</a><br />", esc_url( 'mailto:' . $author_email ), esc_html( $author_email ) );
+ }
+
+ $author_url_line = '';
+ if ( ! empty( $author_url ) ) {
+ $author_url_line = sprintf( "<a href='%1\$s'>%1\$s</a><br />", esc_url( $author_url ) );
+ }
+
+ echo $author_name_line;
+ echo $author_email_line;
+ echo $author_url_line;
+ echo "<a href='edit.php?post_type=feedback&s=" . urlencode( $author_ip );
+ echo "&mode=detail'>" . esc_html( $author_ip ) . '</a><br />';
+ if ( $form_url ) {
+ echo '<a href="' . esc_url( $form_url ) . '">' . esc_html( $form_url ) . '</a>';
+ }
+ break;
+
+ case 'feedback_message':
+ $post_type_object = get_post_type_object( $post->post_type );
+ if ( isset( $content_fields['_feedback_subject'] ) ) {
+ echo '<strong>';
+ echo esc_html( $content_fields['_feedback_subject'] );
+ echo '</strong>';
+ echo '<br />';
+ }
+ echo sanitize_text_field( get_the_content( '' ) );
+ echo '<br />';
+
+ $extra_fields = get_post_meta( $post_id, '_feedback_extra_fields', true );
+ if ( ! empty( $extra_fields ) ) {
+ echo '<br /><hr />';
+ echo '<table cellspacing="0" cellpadding="0" style="">' . "\n";
+ foreach ( (array) $extra_fields as $k => $v ) {
+ // Remove prefix from exta fields
+ echo "<tr><td align='right'><b>" . esc_html( preg_replace( '#^\d+_#', '', $k ) ) . '</b></td><td>' . sanitize_text_field( $v ) . "</td></tr>\n";
+ }
+ echo '</table>';
+ }
+
+ echo '<div class="row-actions">';
+ if ( $post->post_status == 'trash' ) {
+ echo '<span class="untrash" id="feedback-restore-' . $post_id;
+ echo '"><a title="';
+ echo esc_attr__( 'Restore this item from the Trash', 'jetpack' );
+ echo '" href="' . wp_nonce_url( admin_url( sprintf( $post_type_object->_edit_link . '&amp;action=untrash', $post->ID ) ), 'untrash-' . $post->post_type . '_' . $post->ID );
+ echo '">' . __( 'Restore', 'jetpack' ) . '</a></span> | ';
+
+ echo "<span class='delete'> <a class='submitdelete' title='";
+ echo esc_attr( __( 'Delete this item permanently', 'jetpack' ) );
+ echo "' href='" . get_delete_post_link( $post->ID, '', true );
+ echo "'>" . __( 'Delete Permanently', 'jetpack' ) . '</a></span>';
+?>
+
+<script>
+jQuery(document).ready(function($) {
+$('#feedback-restore-<?php echo $post_id; ?>').click(function(e) {
+ e.preventDefault();
+ $.post(ajaxurl, {
+ action: 'grunion_ajax_spam',
+ post_id: '<?php echo $post_id; ?>',
+ make_it: 'publish',
+ sub_menu: jQuery('.subsubsub .current').attr('href'),
+ _ajax_nonce: '<?php echo wp_create_nonce( 'grunion-post-status-' . $post_id ); ?>'
+ },
+ function(r) {
+ $('#post-<?php echo $post_id; ?>')
+ .css({backgroundColor: '#59C859'})
+ .fadeOut(350, function() {
+ $(this).remove();
+ $('.subsubsub').html(r);
+ });
+ }
+ );
+});
+});
+</script>
+
+<?php
+ } elseif ( $post->post_status == 'publish' ) {
+ echo '<span class="spam" id="feedback-spam-' . $post_id;
+ echo '"><a title="';
+ echo __( 'Mark this message as spam', 'jetpack' );
+ echo '" href="' . wp_nonce_url( admin_url( 'admin-ajax.php?post_id=' . $post_id . '&amp;action=spam' ), 'spam-feedback_' . $post_id );
+ echo '">Spam</a></span>';
+ echo ' | ';
+
+ echo '<span class="delete" id="feedback-trash-' . $post_id;
+ echo '">';
+ echo '<a class="submitdelete" title="' . esc_attr__( 'Trash', 'jetpack' );
+ echo '" href="' . get_delete_post_link( $post_id );
+ echo '">' . __( 'Trash', 'jetpack' ) . '</a></span>';
+
+?>
+
+<script>
+jQuery(document).ready( function($) {
+ $('#feedback-spam-<?php echo $post_id; ?>').click( function(e) {
+ e.preventDefault();
+ $.post( ajaxurl, {
+ action: 'grunion_ajax_spam',
+ post_id: '<?php echo $post_id; ?>',
+ make_it: 'spam',
+ sub_menu: jQuery('.subsubsub .current').attr('href'),
+ _ajax_nonce: '<?php echo wp_create_nonce( 'grunion-post-status-' . $post_id ); ?>'
+ },
+ function( r ) {
+ $('#post-<?php echo $post_id; ?>')
+ .css( {backgroundColor:'#FF7979'} )
+ .fadeOut(350, function() {
+ $(this).remove();
+ $('.subsubsub').html(r);
+ });
+ });
+ });
+
+ $('#feedback-trash-<?php echo $post_id; ?>').click(function(e) {
+ e.preventDefault();
+ $.post(ajaxurl, {
+ action: 'grunion_ajax_spam',
+ post_id: '<?php echo $post_id; ?>',
+ make_it: 'trash',
+ sub_menu: jQuery('.subsubsub .current').attr('href'),
+ _ajax_nonce: '<?php echo wp_create_nonce( 'grunion-post-status-' . $post_id ); ?>'
+ },
+ function(r) {
+ $('#post-<?php echo $post_id; ?>')
+ .css({backgroundColor: '#FF7979'})
+ .fadeOut(350, function() {
+ $(this).remove();
+ $('.subsubsub').html(r);
+ });
+ }
+ );
+ });
+});
+</script>
+
+<?php
+ } elseif ( $post->post_status == 'spam' ) {
+ echo '<span class="unspam unapprove" id="feedback-ham-' . $post_id;
+ echo '"><a title="';
+ echo __( 'Mark this message as NOT spam', 'jetpack' );
+ echo '" href="">Not Spam</a></span>';
+ echo ' | ';
+
+ echo "<span class='delete' id='feedback-trash-" . $post_id;
+ echo "'> <a class='submitdelete' title='";
+ echo esc_attr( __( 'Delete this item permanently', 'jetpack' ) );
+ echo "' href='" . get_delete_post_link( $post->ID, '', true );
+ echo "'>" . __( 'Delete Permanently', 'jetpack' ) . '</a></span>';
+?>
+
+<script>
+jQuery(document).ready( function($) {
+ $('#feedback-ham-<?php echo $post_id; ?>').click( function(e) {
+ e.preventDefault();
+ $.post( ajaxurl, {
+ action: 'grunion_ajax_spam',
+ post_id: '<?php echo $post_id; ?>',
+ make_it: 'ham',
+ sub_menu: jQuery('.subsubsub .current').attr('href'),
+ _ajax_nonce: '<?php echo wp_create_nonce( 'grunion-post-status-' . $post_id ); ?>'
+ },
+ function( r ) {
+ $('#post-<?php echo $post_id; ?>')
+ .css( {backgroundColor:'#59C859'} )
+ .fadeOut(350, function() {
+ $(this).remove();
+ $('.subsubsub').html(r);
+ });
+ });
+ });
+});
+</script>
+
+<?php
+ }
+ break;
+
+ case 'feedback_date':
+ $date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
+ $date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
+ $time = date_i18n( $date_time_format, get_the_time( 'U' ) );
+
+ echo $time;
+ break;
+ }
+}
+
+function grunion_esc_attr( $attr ) {
+ $out = esc_attr( $attr );
+ // we also have to entity-encode square brackets so they don't interfere with the shortcode parser
+ // FIXME: do this better - just stripping out square brackets for now since they mysteriously keep reappearing
+ $out = str_replace( '[', '', $out );
+ $out = str_replace( ']', '', $out );
+ return $out;
+}
+
+function grunion_sort_objects( $a, $b ) {
+ if ( isset( $a['order'] ) && isset( $b['order'] ) ) {
+ return $a['order'] - $b['order'];
+ }
+ return 0;
+}
+
+// take an array of field types from the form builder, and construct a shortcode form
+// returns both the shortcode form, and HTML markup representing a preview of the form
+function grunion_ajax_shortcode() {
+ check_ajax_referer( 'grunion_shortcode' );
+
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ die( '-1' );
+ }
+
+ $attributes = array();
+
+ foreach ( array( 'subject', 'to' ) as $attribute ) {
+ if ( isset( $_POST[ $attribute ] ) && strlen( $_POST[ $attribute ] ) ) {
+ $attributes[ $attribute ] = stripslashes( $_POST[ $attribute ] );
+ }
+ }
+
+ if ( is_array( $_POST['fields'] ) ) {
+ $fields = stripslashes_deep( $_POST['fields'] );
+ usort( $fields, 'grunion_sort_objects' );
+
+ $field_shortcodes = array();
+
+ foreach ( $fields as $field ) {
+ $field_attributes = array();
+
+ if ( isset( $field['required'] ) && 'true' === $field['required'] ) {
+ $field_attributes['required'] = 'true';
+ }
+
+ foreach ( array( 'options', 'label', 'type' ) as $attribute ) {
+ if ( isset( $field[ $attribute ] ) ) {
+ $field_attributes[ $attribute ] = $field[ $attribute ];
+ }
+ }
+
+ $field_shortcodes[] = new Grunion_Contact_Form_Field( $field_attributes );
+ }
+ }
+
+ $grunion = new Grunion_Contact_Form( $attributes, $field_shortcodes );
+
+ die( "\n$grunion\n" );
+}
+
+// takes a post_id, extracts the contact-form shortcode from that post (if there is one), parses it,
+// and constructs a json object representing its contents and attributes
+function grunion_ajax_shortcode_to_json() {
+ global $post, $grunion_form;
+
+ check_ajax_referer( 'grunion_shortcode_to_json' );
+
+ if ( ! empty( $_POST['post_id'] ) && ! current_user_can( 'edit_post', $_POST['post_id'] ) ) {
+ die( '-1' );
+ } elseif ( ! current_user_can( 'edit_posts' ) ) {
+ die( '-1' );
+ }
+
+ if ( ! isset( $_POST['content'] ) || ! is_numeric( $_POST['post_id'] ) ) {
+ die( '-1' );
+ }
+
+ $content = stripslashes( $_POST['content'] );
+
+ // doesn't look like a post with a [contact-form] already.
+ if ( false === has_shortcode( $content, 'contact-form' ) ) {
+ die( '' );
+ }
+
+ $post = get_post( $_POST['post_id'] );
+
+ do_shortcode( $content );
+
+ $grunion = Grunion_Contact_Form::$last;
+
+ $out = array(
+ 'to' => '',
+ 'subject' => '',
+ 'fields' => array(),
+ );
+
+ foreach ( $grunion->fields as $field ) {
+ $out['fields'][ $field->get_attribute( 'id' ) ] = $field->attributes;
+ }
+
+ $to = $grunion->get_attribute( 'to' );
+ $subject = $grunion->get_attribute( 'subject' );
+ foreach ( array( 'to', 'subject' ) as $attribute ) {
+ $value = $grunion->get_attribute( $attribute );
+ if ( isset( $grunion->defaults[ $attribute ] ) && $value == $grunion->defaults[ $attribute ] ) {
+ $value = '';
+ }
+ $out[ $attribute ] = $value;
+ }
+
+ die( json_encode( $out ) );
+}
+
+
+add_action( 'wp_ajax_grunion_shortcode', 'grunion_ajax_shortcode' );
+add_action( 'wp_ajax_grunion_shortcode_to_json', 'grunion_ajax_shortcode_to_json' );
+
+
+// process row-action spam/not spam clicks
+add_action( 'wp_ajax_grunion_ajax_spam', 'grunion_ajax_spam' );
+function grunion_ajax_spam() {
+ global $wpdb;
+
+ if ( empty( $_POST['make_it'] ) ) {
+ return;
+ }
+
+ $post_id = (int) $_POST['post_id'];
+ check_ajax_referer( 'grunion-post-status-' . $post_id );
+ if ( ! current_user_can( 'edit_page', $post_id ) ) {
+ wp_die( __( 'You are not allowed to manage this item.', 'jetpack' ) );
+ }
+
+ require_once dirname( __FILE__ ) . '/grunion-contact-form.php';
+
+ $current_menu = '';
+ if ( isset( $_POST['sub_menu'] ) && preg_match( '|post_type=feedback|', $_POST['sub_menu'] ) ) {
+ if ( preg_match( '|post_status=spam|', $_POST['sub_menu'] ) ) {
+ $current_menu = 'spam';
+ } elseif ( preg_match( '|post_status=trash|', $_POST['sub_menu'] ) ) {
+ $current_menu = 'trash';
+ } else {
+ $current_menu = 'messages';
+ }
+ }
+
+ $post = get_post( $post_id );
+ $post_type_object = get_post_type_object( $post->post_type );
+ $akismet_values = get_post_meta( $post_id, '_feedback_akismet_values', true );
+ if ( $_POST['make_it'] == 'spam' ) {
+ $post->post_status = 'spam';
+ $status = wp_insert_post( $post );
+ wp_transition_post_status( 'spam', 'publish', $post );
+
+ /** This action is already documented in modules/contact-form/admin.php */
+ do_action( 'contact_form_akismet', 'spam', $akismet_values );
+ } elseif ( $_POST['make_it'] == 'ham' ) {
+ $post->post_status = 'publish';
+ $status = wp_insert_post( $post );
+ wp_transition_post_status( 'publish', 'spam', $post );
+
+ /** This action is already documented in modules/contact-form/admin.php */
+ do_action( 'contact_form_akismet', 'ham', $akismet_values );
+
+ $comment_author_email = $reply_to_addr = $message = $to = $headers = false;
+ $blog_url = wp_parse_url( site_url() );
+
+ // resend the original email
+ $email = get_post_meta( $post_id, '_feedback_email', true );
+ $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $post_id );
+
+ if ( ! empty( $email ) && ! empty( $content_fields ) ) {
+ if ( isset( $content_fields['_feedback_author_email'] ) ) {
+ $comment_author_email = $content_fields['_feedback_author_email'];
+ }
+
+ if ( isset( $email['to'] ) ) {
+ $to = $email['to'];
+ }
+
+ if ( isset( $email['message'] ) ) {
+ $message = $email['message'];
+ }
+
+ if ( isset( $email['headers'] ) ) {
+ $headers = $email['headers'];
+ } else {
+ $headers = 'From: "' . $content_fields['_feedback_author'] . '" <wordpress@' . $blog_url['host'] . ">\r\n";
+
+ if ( ! empty( $comment_author_email ) ) {
+ $reply_to_addr = $comment_author_email;
+ } elseif ( is_array( $to ) ) {
+ $reply_to_addr = $to[0];
+ }
+
+ if ( $reply_to_addr ) {
+ $headers .= 'Reply-To: "' . $content_fields['_feedback_author'] . '" <' . $reply_to_addr . ">\r\n";
+ }
+
+ $headers .= 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . '"';
+ }
+
+ /**
+ * Filters the subject of the email sent after a contact form submission.
+ *
+ * @module contact-form
+ *
+ * @since 3.0.0
+ *
+ * @param string $content_fields['_feedback_subject'] Feedback's subject line.
+ * @param array $content_fields['_feedback_all_fields'] Feedback's data from old fields.
+ */
+ $subject = apply_filters( 'contact_form_subject', $content_fields['_feedback_subject'], $content_fields['_feedback_all_fields'] );
+
+ Grunion_Contact_Form::wp_mail( $to, $subject, $message, $headers );
+ }
+ } elseif ( $_POST['make_it'] == 'publish' ) {
+ if ( ! current_user_can( $post_type_object->cap->delete_post, $post_id ) ) {
+ wp_die( __( 'You are not allowed to move this item out of the Trash.', 'jetpack' ) );
+ }
+
+ if ( ! wp_untrash_post( $post_id ) ) {
+ wp_die( __( 'Error in restoring from Trash.', 'jetpack' ) );
+ }
+ } elseif ( $_POST['make_it'] == 'trash' ) {
+ if ( ! current_user_can( $post_type_object->cap->delete_post, $post_id ) ) {
+ wp_die( __( 'You are not allowed to move this item to the Trash.', 'jetpack' ) );
+ }
+
+ if ( ! wp_trash_post( $post_id ) ) {
+ wp_die( __( 'Error in moving to Trash.', 'jetpack' ) );
+ }
+ }
+
+ $sql = "
+ SELECT post_status,
+ COUNT( * ) AS post_count
+ FROM `{$wpdb->posts}`
+ WHERE post_type = 'feedback'
+ GROUP BY post_status
+ ";
+ $status_count = (array) $wpdb->get_results( $sql, ARRAY_A );
+
+ $status = array();
+ $status_html = '';
+ foreach ( $status_count as $i => $row ) {
+ $status[ $row['post_status'] ] = $row['post_count'];
+ }
+
+ if ( isset( $status['publish'] ) ) {
+ $status_html .= '<li><a href="edit.php?post_type=feedback"';
+ if ( $current_menu == 'messages' ) {
+ $status_html .= ' class="current"';
+ }
+
+ $status_html .= '>' . __( 'Messages', 'jetpack' ) . ' <span class="count">';
+ $status_html .= '(' . number_format( $status['publish'] ) . ')';
+ $status_html .= '</span></a> |</li>';
+ }
+
+ if ( isset( $status['trash'] ) ) {
+ $status_html .= '<li><a href="edit.php?post_status=trash&amp;post_type=feedback"';
+ if ( $current_menu == 'trash' ) {
+ $status_html .= ' class="current"';
+ }
+
+ $status_html .= '>' . __( 'Trash', 'jetpack' ) . ' <span class="count">';
+ $status_html .= '(' . number_format( $status['trash'] ) . ')';
+ $status_html .= '</span></a>';
+ if ( isset( $status['spam'] ) ) {
+ $status_html .= ' |';
+ }
+ $status_html .= '</li>';
+ }
+
+ if ( isset( $status['spam'] ) ) {
+ $status_html .= '<li><a href="edit.php?post_status=spam&amp;post_type=feedback"';
+ if ( $current_menu == 'spam' ) {
+ $status_html .= ' class="current"';
+ }
+
+ $status_html .= '>' . __( 'Spam', 'jetpack' ) . ' <span class="count">';
+ $status_html .= '(' . number_format( $status['spam'] ) . ')';
+ $status_html .= '</span></a></li>';
+ }
+
+ echo $status_html;
+ exit;
+}
+
+/**
+ * Add the scripts that will add the "Check for Spam" button to the Feedbacks dashboard page.
+ */
+function grunion_enable_spam_recheck() {
+ if ( ! defined( 'AKISMET_VERSION' ) ) {
+ return;
+ }
+
+ $screen = get_current_screen();
+
+ // Only add to feedback, only to non-spam view
+ if ( 'edit-feedback' != $screen->id || ( ! empty( $_GET['post_status'] ) && 'spam' == $_GET['post_status'] ) ) {
+ return;
+ }
+
+ // Add the scripts that handle the spam check event.
+ wp_register_script(
+ 'grunion-admin',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/contact-form/js/grunion-admin.min.js',
+ 'modules/contact-form/js/grunion-admin.js'
+ ),
+ array( 'jquery' )
+ );
+ wp_enqueue_script( 'grunion-admin' );
+
+ wp_enqueue_style( 'grunion.css' );
+
+ // Add the actual "Check for Spam" button.
+ add_action( 'admin_head', 'grunion_check_for_spam_button' );
+}
+
+add_action( 'admin_enqueue_scripts', 'grunion_enable_spam_recheck' );
+
+/**
+ * Add the "Check for Spam" button to the Feedbacks dashboard page.
+ */
+function grunion_check_for_spam_button() {
+ // Get HTML for the button
+ $button_html = get_submit_button(
+ __( 'Check for Spam', 'jetpack' ),
+ 'secondary',
+ 'jetpack-check-feedback-spam',
+ false,
+ array( 'class' => 'jetpack-check-feedback-spam' )
+ );
+ $button_html .= '<span class="jetpack-check-feedback-spam-spinner"></span>';
+
+ // Add the button next to the filter button via js
+ ?>
+ <script type="text/javascript">
+ jQuery( function( $ ) {
+ $( '#posts-filter #post-query-submit' ).after( '<?php echo $button_html; ?>' );
+ } );
+ </script>
+ <?php
+}
+
+/**
+ * Recheck all approved feedbacks for spam.
+ */
+function grunion_recheck_queue() {
+ global $wpdb;
+
+ $query = 'post_type=feedback&post_status=publish';
+
+ if ( isset( $_POST['limit'], $_POST['offset'] ) ) {
+ $query .= '&posts_per_page=' . intval( $_POST['limit'] ) . '&offset=' . intval( $_POST['offset'] );
+ }
+
+ $approved_feedbacks = get_posts( $query );
+
+ foreach ( $approved_feedbacks as $feedback ) {
+ $meta = get_post_meta( $feedback->ID, '_feedback_akismet_values', true );
+
+ /**
+ * Filter whether the submitted feedback is considered as spam.
+ *
+ * @module contact-form
+ *
+ * @since 3.4.0
+ *
+ * @param bool false Is the submitted feedback spam? Default to false.
+ * @param array $meta Feedack values returned by the Akismet plugin.
+ */
+ $is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $meta );
+
+ if ( $is_spam ) {
+ wp_update_post(
+ array(
+ 'ID' => $feedback->ID,
+ 'post_status' => 'spam',
+ )
+ );
+ /** This action is already documented in modules/contact-form/admin.php */
+ do_action( 'contact_form_akismet', 'spam', $meta );
+ }
+ }
+
+ wp_send_json(
+ array(
+ 'processed' => count( $approved_feedbacks ),
+ )
+ );
+}
+
+add_action( 'wp_ajax_grunion_recheck_queue', 'grunion_recheck_queue' );
diff --git a/plugins/jetpack/modules/contact-form/class-grunion-contact-form-endpoint.php b/plugins/jetpack/modules/contact-form/class-grunion-contact-form-endpoint.php
new file mode 100644
index 00000000..ba2785a6
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/class-grunion-contact-form-endpoint.php
@@ -0,0 +1,52 @@
+<?php
+
+/*
+ * Plugin Name: Feedback CPT Permissions over-ride
+ */
+
+if ( class_exists( 'WP_REST_Posts_Controller' ) ) {
+
+ /**
+ * Class Grunion_Contact_Form_Endpoint
+ * Used as 'rest_controller_class' parameter when 'feedback' post type is registered in modules/contact-form/grunion-contact-form.php.
+ */
+ class Grunion_Contact_Form_Endpoint extends WP_REST_Posts_Controller {
+ /**
+ * Check whether a given request has proper authorization to view feedback items.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_Error|boolean
+ */
+ public function get_items_permissions_check( $request ) {
+ if ( ! is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
+ return new WP_Error(
+ 'rest_cannot_view',
+ esc_html__( 'Sorry, you cannot view this resource.', 'jetpack' ),
+ array( 'status' => 401 )
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Check whether a given request has proper authorization to view feedback item.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_Error|boolean
+ */
+ public function get_item_permissions_check( $request ) {
+ if ( ! is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
+ return new WP_Error(
+ 'rest_cannot_view',
+ esc_html__( 'Sorry, you cannot view this resource.', 'jetpack' ),
+ array( 'status' => 401 )
+ );
+ }
+
+ return true;
+ }
+
+ }
+
+}
diff --git a/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style-rtl.css b/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style-rtl.css
new file mode 100644
index 00000000..1cfbf3ce
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style-rtl.css
@@ -0,0 +1,825 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+/* ==========================================================================
+** Normalize
+** ======================================================================== */
+
+html {
+ direction: rtl;
+}
+
+body {
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
+ font-size: 16px;
+ line-height: 1.4em;
+ margin: 0;
+}
+
+/* Links */
+a,
+a:visited {
+ color: #0087be;
+ text-decoration: none;
+}
+
+a:hover,
+a:focus,
+a:active {
+ color: $link-highlight;
+}
+
+/* ==========================================================================
+** Card
+** ======================================================================= */
+
+.card,
+body {
+ display: block;
+ position: relative;
+ margin: 0 auto 10px auto;
+ padding: 16px;
+ box-sizing: border-box;
+ background: white;
+ box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
+}
+
+body {
+ margin: 0;
+ background: #f5f5f5;
+}
+
+.card:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+.card:hover,
+.card:focus {
+ box-shadow: 0 0 0 1px #999, 0 1px 2px #e9eff3;
+}
+
+.card .delete-field {
+ display: block;
+ float: left;
+}
+
+@media ( min-width: 481px ) {
+ .card {
+ margin-bottom: 16px;
+ padding: 24px;
+ }
+ body {
+ padding: 24px;
+ }
+}
+
+.card.is-compact {
+ margin-bottom: 1px;
+}
+
+@media ( min-width: 481px ) {
+ .card.is-compact {
+ margin-bottom: 1px;
+ padding: 16px 24px;
+ }
+}
+
+.card > div {
+ margin-top: 24px;
+}
+
+.card > div:first-child {
+ margin-top: 0;
+}
+
+
+/* ==========================================================================
+** Labels
+** ======================================================================= */
+
+label {
+ display: block;
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 5px;
+ margin-top: 8px;
+}
+
+label:first-of-type {
+ margin-top: 4px;
+}
+
+
+/* ==========================================================================
+** Text Inputs
+** ======================================================================= */
+
+input[type="text"],
+input[type="tel"],
+input[type="email"],
+input[type="url"] {
+ border-radius: 0;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 7px 14px;
+ width: 100%;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 1.5;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-shadow: none;
+}
+
+input[type="text"]:-ms-input-placeholder,
+input[type="tel"]:-ms-input-placeholder,
+input[type="email"]:-ms-input-placeholder,
+input[type="url"]:-ms-input-placeholder {
+ color: #87a6bc;
+}
+
+input[type="text"]::-ms-input-placeholder,
+input[type="tel"]::-ms-input-placeholder,
+input[type="email"]::-ms-input-placeholder,
+input[type="url"]::-ms-input-placeholder {
+ color: #87a6bc;
+}
+
+input[type="text"]::placeholder,
+input[type="tel"]::placeholder,
+input[type="email"]::placeholder,
+input[type="url"]::placeholder {
+ color: #87a6bc;
+}
+
+input[type="text"]:hover,
+input[type="tel"]:hover,
+input[type="email"]:hover,
+input[type="url"]:hover {
+ border-color: #a8bece;
+}
+
+input[type="text"]:focus,
+input[type="tel"]:focus,
+input[type="email"]:focus,
+input[type="url"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+input[type="text"]:focus::-ms-clear,
+input[type="tel"]:focus::-ms-clear,
+input[type="email"]:focus::-ms-clear,
+input[type="url"]:focus::-ms-clear {
+ display: none;
+}
+
+input[type="text"]:disabled,
+input[type="tel"]:disabled,
+input[type="email"]:disabled,
+input[type="url"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ -webkit-text-fill-color: #a8bece;
+}
+
+input[type="text"]:disabled:hover,
+input[type="tel"]:disabled:hover,
+input[type="email"]:disabled:hover,
+input[type="url"]:disabled:hover {
+ cursor: default;
+}
+
+input[type="text"]:disabled:-ms-input-placeholder,
+input[type="tel"]:disabled:-ms-input-placeholder,
+input[type="email"]:disabled:-ms-input-placeholder,
+input[type="url"]:disabled:-ms-input-placeholder {
+ color: #a8bece;
+}
+
+input[type="text"]:disabled::-ms-input-placeholder,
+input[type="tel"]:disabled::-ms-input-placeholder,
+input[type="email"]:disabled::-ms-input-placeholder,
+input[type="url"]:disabled::-ms-input-placeholder {
+ color: #a8bece;
+}
+
+input[type="text"]:disabled::placeholder,
+input[type="tel"]:disabled::placeholder,
+input[type="email"]:disabled::placeholder,
+input[type="url"]:disabled::placeholder {
+ color: #a8bece;
+}
+
+
+/* ==========================================================================
+** Textareas
+** ======================================================================= */
+
+textarea {
+ border-radius: 0;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 7px 14px;
+ height: 92px;
+ width: 100%;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 1.5;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-shadow: none;
+}
+
+textarea:-ms-input-placeholder {
+ color: #87a6bc;
+}
+
+textarea::-ms-input-placeholder {
+ color: #87a6bc;
+}
+
+textarea::placeholder {
+ color: #87a6bc;
+}
+
+textarea:hover {
+ border-color: #a8bece;
+}
+
+textarea:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+textarea:focus::-ms-clear {
+ display: none;
+}
+
+textarea:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ -webkit-text-fill-color: #a8bece;
+}
+
+textarea:disabled:hover {
+ cursor: default;
+}
+
+textarea:disabled:-ms-input-placeholder {
+ color: #a8bece;
+}
+
+textarea:disabled::-ms-input-placeholder {
+ color: #a8bece;
+}
+
+textarea:disabled::placeholder {
+ color: #a8bece;
+}
+
+
+/* ==========================================================================
+** Checkboxes
+** ======================================================================= */
+
+.checkbox,
+input[type="checkbox"] {
+ -webkit-appearance: none;
+ display: inline-block;
+ box-sizing: border-box;
+ margin: 2px 0 0;
+ padding: 7px 14px;
+ width: 16px;
+ height: 16px;
+ float: right;
+ outline: 0;
+ padding: 0;
+ box-shadow: none;
+ background-color: #fff;
+ border: 1px solid #c8d7e1;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 0;
+ text-align: center;
+ vertical-align: middle;
+ -moz-appearance: none;
+ appearance: none;
+ transition: all .15s ease-in-out;
+ clear: none;
+ cursor: pointer;
+}
+
+.checkbox:checked:before,
+input[type="checkbox"]:checked:before {
+ content: '\f147';
+ font-family: Dashicons;
+ margin: -3px -4px 0 0;
+ float: right;
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ font-size: 20px;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ speak: none;
+ color: #00aadc;
+}
+
+.checkbox:disabled:checked:before,
+input[type="checkbox"]:disabled:checked:before {
+ color: #a8bece;
+}
+
+.checkbox:hover,
+input[type="checkbox"]:hover {
+ border-color: #a8bece;
+}
+
+.checkbox:focus,
+input[type="checkbox"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+.checkbox:disabled,
+input[type="checkbox"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ opacity: 1;
+}
+
+.checkbox:disabled:hover,
+input[type="checkbox"]:disabled:hover {
+ cursor: default;
+}
+
+.checkbox + span,
+input[type="checkbox"] + span {
+ display: block;
+ font-weight: normal;
+ margin-right: 24px;
+}
+
+
+/* ==========================================================================
+** Radio buttons
+** ======================================================================== */
+
+.radio-button,
+input[type=radio] {
+ color: #2e4453;
+ font-size: 16px;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-sizing: border-box;
+ -webkit-appearance: none;
+ clear: none;
+ cursor: pointer;
+ display: inline-block;
+ line-height: 0;
+ height: 16px;
+ margin: 2px 0 0 4px;
+ float: right;
+ outline: 0;
+ padding: 0;
+ text-align: center;
+ vertical-align: middle;
+ width: 16px;
+ min-width: 16px;
+ -moz-appearance: none;
+ appearance: none;
+ border-radius: 50%;
+ line-height: 10px;
+}
+
+.radio-button:hover,
+input[type="radio"]:hover {
+ border-color: #a8bece;
+}
+
+.radio-button:focus,
+input[type="radio"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+.radio-button:focus::-ms-clear,
+input[type="radio"]:focus::-ms-clear {
+ display: none;
+}
+
+.radio-button:checked:before,
+input[type="radio"]:checked:before {
+ float: right;
+ display: inline-block;
+ content: '\2022';
+ margin: 3px;
+ width: 8px;
+ height: 8px;
+ text-indent: -9999px;
+ background: #00aadc;
+ vertical-align: middle;
+ border-radius: 50%;
+ animation: grow .2s ease-in-out;
+}
+
+.radio-button:disabled,
+input[type="radio"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ opacity: 1;
+ -webkit-text-fill-color: #a8bece;
+}
+
+.radio-button:disabled:hover,
+input[type="radio"]:disabled:hover {
+ cursor: default;
+}
+
+.radio-button:disabled:-ms-input-placeholder,
+input[type="radio"]:disabled:-ms-input-placeholder {
+ color: #a8bece;
+}
+
+.radio-button:disabled::-ms-input-placeholder,
+input[type="radio"]:disabled::-ms-input-placeholder {
+ color: #a8bece;
+}
+
+.radio-button:disabled::placeholder,
+input[type="radio"]:disabled::placeholder {
+ color: #a8bece;
+}
+
+.radio-button:disabled:checked::before,
+input[type="radio"]:disabled:checked:before {
+ background: #e9eff3;
+}
+
+.radio-button + span,
+input[type="radio"] + span {
+ display: block;
+ font-weight: normal;
+ margin-right: 24px;
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale(0.3);
+ }
+
+ 60% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale(0.3);
+ }
+
+ 60% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+
+/* ==========================================================================
+** Selects
+** ======================================================================== */
+
+select {
+ background: #fff url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjQzhEN0UxIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat left 10px center;
+ border-color: #c8d7e1;
+ border-style: solid;
+ border-radius: 4px;
+ border-width: 1px 1px 2px;
+ color: #2e4453;
+ cursor: pointer;
+ display: inline-block;
+ margin: 0;
+ outline: 0;
+ overflow: hidden;
+ font-size: 14px;
+ line-height: 21px;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ white-space: nowrap;
+ box-sizing: border-box;
+ /* Aligns the text to the 8px baseline grid and adds padding on right to allow for the arrow. */
+ padding: 7px 14px 9px 32px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+select:hover {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjYThiZWNlIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==);
+}
+
+select:focus {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiA8dGl0bGU+YXJyb3ctZG93bjwvdGl0bGU+IDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiA8ZGVmcz48L2RlZnM+IDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPiA8ZyBpZD0iYXJyb3ctZG93biIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgZmlsbD0iIzJlNDQ1MyI+IDxwYXRoIGQ9Ik0xNS41LDYgTDE3LDcuNSBMMTAuMjUsMTQuMjUgTDMuNSw3LjUgTDUsNiBMMTAuMjUsMTEuMjUgTDE1LjUsNiBaIiBpZD0iRG93bi1BcnJvdyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPiA8L2c+IDwvZz48L3N2Zz4=);
+ border-color: #00aadc;
+ box-shadow: 0 0 0 2px #78dcfa;
+ outline: 0;
+ -moz-outline:none;
+ -moz-user-focus:ignore;
+}
+
+select:disabled,
+select:hover:disabled {
+ background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjZTllZmYzIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat left 10px center;;
+}
+
+select.is-compact {
+ min-width: 0;
+ padding: 0 6px 2px 20px;
+ margin: 0 4px;
+ background-position: left 5px center;
+ background-size: 12px 12px;
+}
+
+/* Make it display:block when it follows a label */
+label select,
+label + select {
+ display: block;
+ min-width: 200px;
+}
+
+label select.is-compact,
+label + select.is-compact {
+ display: inline-block;
+ min-width: 0;
+}
+
+/* IE: Remove the default arrow */
+select::-ms-expand {
+ display: none;
+}
+
+/* IE: Remove default background and color styles on focus */
+select::-ms-value {
+ background: none;
+ color: #2e4453;
+}
+
+/* Firefox: Remove the focus outline, see http://stackoverflow.com/questions/3773430/remove-outline-from-select-box-in-ff/18853002#18853002 */
+select:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #2e4453;
+}
+
+
+/* ==========================================================================
+** Buttons
+** ======================================================================== */
+
+input[type="submit"] {
+ padding: 0;
+ font-size: 14px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ vertical-align: baseline;
+ background: white;
+ border-color: #c8d7e1;
+ border-style: solid;
+ border-width: 1px 1px 2px;
+ color: #2e4453;
+ cursor: pointer;
+ display: inline-block;
+ margin: 24px 0 0;
+ outline: 0;
+ overflow: hidden;
+ font-weight: 500;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ box-sizing: border-box;
+ font-size: 14px;
+ line-height: 21px;
+ border-radius: 4px;
+ padding: 7px 14px 9px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+input[type="submit"]:hover {
+ border-color: #a8bece;
+ color: #2e4453;
+}
+
+input[type="submit"]:active {
+ border-width: 2px 1px 1px;
+}
+
+input[type="submit"]:visited {
+ color: #2e4453;
+}
+
+input[type="submit"][disabled],
+input[type="submit"]:disabled {
+ color: #e9eff3;
+ background: white;
+ border-color: #e9eff3;
+ cursor: default;
+}
+
+input[type="submit"][disabled]:active,
+input[type="submit"]:disabled:active {
+ border-width: 1px 1px 2px;
+}
+
+input[type="submit"]:focus {
+ border-color: #00aadc;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+input[type="submit"].hidden {
+ display: none;
+}
+
+input[type="submit"] .gridicon {
+ position: relative;
+ top: 4px;
+ margin-top: -2px;
+ width: 18px;
+ height: 18px;
+}
+
+input[type="submit"].button-primary {
+ background: #00aadc;
+ border-color: #008ab3;
+ color: white;
+}
+
+input[type="submit"].button-primary:hover,
+input[type="submit"].button-primary:focus {
+ border-color: #005082;
+ color: white;
+}
+
+input[type="submit"].button-primary[disabled],
+input[type="submit"].button-primary:disabled {
+ background: #bceefd;
+ border-color: #8cc9e2;
+ color: white;
+}
+
+input[type="submit"].button-primary {
+ color: white;
+}
+
+
+/* ==========================================================================
+** Inline editor styles
+** ======================================================================== */
+
+
+.ui-sortable-handle {
+ cursor: move;
+}
+
+.grunion-section-header {
+ font-size: 21px;
+ margin-top: 32px;
+ font-weight: 600;
+}
+
+.grunion-form-settings:hover {
+ box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
+}
+
+.grunion-section-header:first-child {
+ margin-top: 0;
+}
+
+.grunion-type-options {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.grunion-type {
+ flex-grow: 0;
+ flex-shrink: 0;
+}
+
+.grunion-type select {
+ -webkit-appearance: none;
+ width: 100%;
+}
+
+.grunion-required {
+ padding: 27px 16px 0 0;
+ flex-grow: 0;
+ flex-shrink: 0;
+}
+
+.grunion-options {
+ padding-top: 16px;
+}
+
+.grunion-options ol {
+ list-style: none;
+ padding: 0;
+ margin: 8px 0 0;
+}
+
+.grunion-options li {
+ display: flex;
+ margin-bottom: 16px;
+}
+
+.grunion-field-edit .grunion-options {
+ display: none;
+}
+
+.delete-option,
+.delete-field {
+ color: #0087be;
+ text-decoration: none;
+ width: 40px;
+ line-height: 40px;
+ font-size: 21px;
+ text-align: center;
+ font-weight: 600;
+}
+
+.delete-field {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.grunion-controls {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.grunion-update-controls {
+ text-align: left;
+ flex-grow: 1;
+}
+
+#add-field {
+ flex-grow: 0;
+}
+
+.delete-option:before,
+.delete-field:before {
+ font-family: Dashicons;
+/* content: "\f158"; /* This is the bolder X */
+ content: "\f335"; /* This is the thinner X */
+ display: inline-block;
+ speak: none;
+}
+
+.grunion-field-edit.grunion-field-checkbox-multiple .grunion-options,
+.grunion-field-edit.grunion-field-radio .grunion-options,
+.grunion-field-edit.grunion-field-select .grunion-options {
+ display: block;
+}
+
+.screen-reader-text {
+ position: absolute;
+ margin: -1px;
+ padding: 0;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ border: 0;
+ word-wrap: normal !important; /* many screen reader and browser combinations announce broken words as they would appear visually */
+}
diff --git a/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style-rtl.min.css b/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style-rtl.min.css
new file mode 100644
index 00000000..9b7a27c2
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style-rtl.min.css
@@ -0,0 +1 @@
+html{direction:rtl}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;line-height:1.4em;margin:0}a,a:visited{color:#0087be;text-decoration:none}a:active,a:focus,a:hover{color:$link-highlight}.card,body{display:block;position:relative;margin:0 auto 10px auto;padding:16px;box-sizing:border-box;background:#fff;box-shadow:0 0 0 1px rgba(200,215,225,.5),0 1px 2px #e9eff3}body{margin:0;background:#f5f5f5}.card:after{content:".";display:block;height:0;clear:both;visibility:hidden}.card:focus,.card:hover{box-shadow:0 0 0 1px #999,0 1px 2px #e9eff3}.card .delete-field{display:block;float:left}@media (min-width:481px){.card{margin-bottom:16px;padding:24px}body{padding:24px}}.card.is-compact{margin-bottom:1px}@media (min-width:481px){.card.is-compact{margin-bottom:1px;padding:16px 24px}}.card>div{margin-top:24px}.card>div:first-child{margin-top:0}label{display:block;font-size:14px;font-weight:600;margin-bottom:5px;margin-top:8px}label:first-of-type{margin-top:4px}input[type=email],input[type=tel],input[type=text],input[type=url]{border-radius:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0;padding:7px 14px;width:100%;color:#2e4453;font-size:16px;line-height:1.5;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-shadow:none}input[type=email]:-ms-input-placeholder,input[type=tel]:-ms-input-placeholder,input[type=text]:-ms-input-placeholder,input[type=url]:-ms-input-placeholder{color:#87a6bc}input[type=email]::-ms-input-placeholder,input[type=tel]::-ms-input-placeholder,input[type=text]::-ms-input-placeholder,input[type=url]::-ms-input-placeholder{color:#87a6bc}input[type=email]::placeholder,input[type=tel]::placeholder,input[type=text]::placeholder,input[type=url]::placeholder{color:#87a6bc}input[type=email]:hover,input[type=tel]:hover,input[type=text]:hover,input[type=url]:hover{border-color:#a8bece}input[type=email]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}input[type=email]:focus::-ms-clear,input[type=tel]:focus::-ms-clear,input[type=text]:focus::-ms-clear,input[type=url]:focus::-ms-clear{display:none}input[type=email]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=url]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;-webkit-text-fill-color:#a8bece}input[type=email]:disabled:hover,input[type=tel]:disabled:hover,input[type=text]:disabled:hover,input[type=url]:disabled:hover{cursor:default}input[type=email]:disabled:-ms-input-placeholder,input[type=tel]:disabled:-ms-input-placeholder,input[type=text]:disabled:-ms-input-placeholder,input[type=url]:disabled:-ms-input-placeholder{color:#a8bece}input[type=email]:disabled::-ms-input-placeholder,input[type=tel]:disabled::-ms-input-placeholder,input[type=text]:disabled::-ms-input-placeholder,input[type=url]:disabled::-ms-input-placeholder{color:#a8bece}input[type=email]:disabled::placeholder,input[type=tel]:disabled::placeholder,input[type=text]:disabled::placeholder,input[type=url]:disabled::placeholder{color:#a8bece}textarea{border-radius:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0;padding:7px 14px;height:92px;width:100%;color:#2e4453;font-size:16px;line-height:1.5;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-shadow:none}textarea:-ms-input-placeholder{color:#87a6bc}textarea::-ms-input-placeholder{color:#87a6bc}textarea::placeholder{color:#87a6bc}textarea:hover{border-color:#a8bece}textarea:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}textarea:focus::-ms-clear{display:none}textarea:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;-webkit-text-fill-color:#a8bece}textarea:disabled:hover{cursor:default}textarea:disabled:-ms-input-placeholder{color:#a8bece}textarea:disabled::-ms-input-placeholder{color:#a8bece}textarea:disabled::placeholder{color:#a8bece}.checkbox,input[type=checkbox]{-webkit-appearance:none;display:inline-block;box-sizing:border-box;margin:2px 0 0;padding:7px 14px;width:16px;height:16px;float:right;outline:0;padding:0;box-shadow:none;background-color:#fff;border:1px solid #c8d7e1;color:#2e4453;font-size:16px;line-height:0;text-align:center;vertical-align:middle;-moz-appearance:none;appearance:none;transition:all .15s ease-in-out;clear:none;cursor:pointer}.checkbox:checked:before,input[type=checkbox]:checked:before{content:'\f147';font-family:Dashicons;margin:-3px -4px 0 0;float:right;display:inline-block;vertical-align:middle;width:16px;font-size:20px;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:none;color:#00aadc}.checkbox:disabled:checked:before,input[type=checkbox]:disabled:checked:before{color:#a8bece}.checkbox:hover,input[type=checkbox]:hover{border-color:#a8bece}.checkbox:focus,input[type=checkbox]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}.checkbox:disabled,input[type=checkbox]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;opacity:1}.checkbox:disabled:hover,input[type=checkbox]:disabled:hover{cursor:default}.checkbox+span,input[type=checkbox]+span{display:block;font-weight:400;margin-right:24px}.radio-button,input[type=radio]{color:#2e4453;font-size:16px;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-sizing:border-box;-webkit-appearance:none;clear:none;cursor:pointer;display:inline-block;line-height:0;height:16px;margin:2px 0 0 4px;float:right;outline:0;padding:0;text-align:center;vertical-align:middle;width:16px;min-width:16px;-moz-appearance:none;appearance:none;border-radius:50%;line-height:10px}.radio-button:hover,input[type=radio]:hover{border-color:#a8bece}.radio-button:focus,input[type=radio]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}.radio-button:focus::-ms-clear,input[type=radio]:focus::-ms-clear{display:none}.radio-button:checked:before,input[type=radio]:checked:before{float:right;display:inline-block;content:'\2022';margin:3px;width:8px;height:8px;text-indent:-9999px;background:#00aadc;vertical-align:middle;border-radius:50%;animation:grow .2s ease-in-out}.radio-button:disabled,input[type=radio]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;opacity:1;-webkit-text-fill-color:#a8bece}.radio-button:disabled:hover,input[type=radio]:disabled:hover{cursor:default}.radio-button:disabled:-ms-input-placeholder,input[type=radio]:disabled:-ms-input-placeholder{color:#a8bece}.radio-button:disabled::-ms-input-placeholder,input[type=radio]:disabled::-ms-input-placeholder{color:#a8bece}.radio-button:disabled::placeholder,input[type=radio]:disabled::placeholder{color:#a8bece}.radio-button:disabled:checked::before,input[type=radio]:disabled:checked:before{background:#e9eff3}.radio-button+span,input[type=radio]+span{display:block;font-weight:400;margin-right:24px}@keyframes grow{0%{transform:scale(.3)}60%{transform:scale(1.15)}100%{transform:scale(1)}}@keyframes grow{0%{transform:scale(.3)}60%{transform:scale(1.15)}100%{transform:scale(1)}}select{background:#fff url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjQzhEN0UxIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat left 10px center;border-color:#c8d7e1;border-style:solid;border-radius:4px;border-width:1px 1px 2px;color:#2e4453;cursor:pointer;display:inline-block;margin:0;outline:0;overflow:hidden;font-size:14px;line-height:21px;font-weight:600;text-overflow:ellipsis;text-decoration:none;vertical-align:top;white-space:nowrap;box-sizing:border-box;padding:7px 14px 9px 32px;-webkit-appearance:none;-moz-appearance:none;appearance:none}select:hover{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjYThiZWNlIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==)}select:focus{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiA8dGl0bGU+YXJyb3ctZG93bjwvdGl0bGU+IDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiA8ZGVmcz48L2RlZnM+IDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPiA8ZyBpZD0iYXJyb3ctZG93biIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgZmlsbD0iIzJlNDQ1MyI+IDxwYXRoIGQ9Ik0xNS41LDYgTDE3LDcuNSBMMTAuMjUsMTQuMjUgTDMuNSw3LjUgTDUsNiBMMTAuMjUsMTEuMjUgTDE1LjUsNiBaIiBpZD0iRG93bi1BcnJvdyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPiA8L2c+IDwvZz48L3N2Zz4=);border-color:#00aadc;box-shadow:0 0 0 2px #78dcfa;outline:0;-moz-outline:none;-moz-user-focus:ignore}select:disabled,select:hover:disabled{background:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjZTllZmYzIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat left 10px center}select.is-compact{min-width:0;padding:0 6px 2px 20px;margin:0 4px;background-position:left 5px center;background-size:12px 12px}label select,label+select{display:block;min-width:200px}label select.is-compact,label+select.is-compact{display:inline-block;min-width:0}select::-ms-expand{display:none}select::-ms-value{background:0 0;color:#2e4453}select:-moz-focusring{color:transparent;text-shadow:0 0 0 #2e4453}input[type=submit]{padding:0;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;vertical-align:baseline;background:#fff;border-color:#c8d7e1;border-style:solid;border-width:1px 1px 2px;color:#2e4453;cursor:pointer;display:inline-block;margin:24px 0 0;outline:0;overflow:hidden;font-weight:500;text-overflow:ellipsis;text-decoration:none;vertical-align:top;box-sizing:border-box;font-size:14px;line-height:21px;border-radius:4px;padding:7px 14px 9px;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=submit]:hover{border-color:#a8bece;color:#2e4453}input[type=submit]:active{border-width:2px 1px 1px}input[type=submit]:visited{color:#2e4453}input[type=submit]:disabled,input[type=submit][disabled]{color:#e9eff3;background:#fff;border-color:#e9eff3;cursor:default}input[type=submit]:disabled:active,input[type=submit][disabled]:active{border-width:1px 1px 2px}input[type=submit]:focus{border-color:#00aadc;box-shadow:0 0 0 2px #78dcfa}input[type=submit].hidden{display:none}input[type=submit] .gridicon{position:relative;top:4px;margin-top:-2px;width:18px;height:18px}input[type=submit].button-primary{background:#00aadc;border-color:#008ab3;color:#fff}input[type=submit].button-primary:focus,input[type=submit].button-primary:hover{border-color:#005082;color:#fff}input[type=submit].button-primary:disabled,input[type=submit].button-primary[disabled]{background:#bceefd;border-color:#8cc9e2;color:#fff}input[type=submit].button-primary{color:#fff}.ui-sortable-handle{cursor:move}.grunion-section-header{font-size:21px;margin-top:32px;font-weight:600}.grunion-form-settings:hover{box-shadow:0 0 0 1px rgba(200,215,225,.5),0 1px 2px #e9eff3}.grunion-section-header:first-child{margin-top:0}.grunion-type-options{display:flex;flex-wrap:wrap}.grunion-type{flex-grow:0;flex-shrink:0}.grunion-type select{-webkit-appearance:none;width:100%}.grunion-required{padding:27px 16px 0 0;flex-grow:0;flex-shrink:0}.grunion-options{padding-top:16px}.grunion-options ol{list-style:none;padding:0;margin:8px 0 0}.grunion-options li{display:flex;margin-bottom:16px}.grunion-field-edit .grunion-options{display:none}.delete-field,.delete-option{color:#0087be;text-decoration:none;width:40px;line-height:40px;font-size:21px;text-align:center;font-weight:600}.delete-field{position:absolute;top:0;left:0}.grunion-controls{display:flex;flex-wrap:wrap}.grunion-update-controls{text-align:left;flex-grow:1}#add-field{flex-grow:0}.delete-field:before,.delete-option:before{font-family:Dashicons;content:"\f335";display:inline-block;speak:none}.grunion-field-edit.grunion-field-checkbox-multiple .grunion-options,.grunion-field-edit.grunion-field-radio .grunion-options,.grunion-field-edit.grunion-field-select .grunion-options{display:block}.screen-reader-text{position:absolute;margin:-1px;padding:0;height:1px;width:1px;overflow:hidden;clip:rect(0 0 0 0);border:0;word-wrap:normal!important} \ No newline at end of file
diff --git a/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style.css b/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style.css
new file mode 100644
index 00000000..13db02fd
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style.css
@@ -0,0 +1,764 @@
+/* ==========================================================================
+** Normalize
+** ======================================================================== */
+
+html {
+ direction: ltr;
+}
+
+body {
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
+ font-size: 16px;
+ line-height: 1.4em;
+ margin: 0;
+}
+
+/* Links */
+a,
+a:visited {
+ color: #0087be;
+ text-decoration: none;
+}
+
+a:hover,
+a:focus,
+a:active {
+ color: $link-highlight;
+}
+
+/* ==========================================================================
+** Card
+** ======================================================================= */
+
+.card,
+body {
+ display: block;
+ position: relative;
+ margin: 0 auto 10px auto;
+ padding: 16px;
+ box-sizing: border-box;
+ background: white;
+ box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
+}
+
+body {
+ margin: 0;
+ background: #f5f5f5;
+}
+
+.card:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+.card:hover,
+.card:focus {
+ box-shadow: 0 0 0 1px #999, 0 1px 2px #e9eff3;
+}
+
+.card .delete-field {
+ display: block;
+ float: right;
+}
+
+@media ( min-width: 481px ) {
+ .card {
+ margin-bottom: 16px;
+ padding: 24px;
+ }
+ body {
+ padding: 24px;
+ }
+}
+
+.card.is-compact {
+ margin-bottom: 1px;
+}
+
+@media ( min-width: 481px ) {
+ .card.is-compact {
+ margin-bottom: 1px;
+ padding: 16px 24px;
+ }
+}
+
+.card > div {
+ margin-top: 24px;
+}
+
+.card > div:first-child {
+ margin-top: 0;
+}
+
+
+/* ==========================================================================
+** Labels
+** ======================================================================= */
+
+label {
+ display: block;
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 5px;
+ margin-top: 8px;
+}
+
+label:first-of-type {
+ margin-top: 4px;
+}
+
+
+/* ==========================================================================
+** Text Inputs
+** ======================================================================= */
+
+input[type="text"],
+input[type="tel"],
+input[type="email"],
+input[type="url"] {
+ border-radius: 0;
+ appearance: none;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 7px 14px;
+ width: 100%;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 1.5;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-shadow: none;
+}
+
+input[type="text"]::placeholder,
+input[type="tel"]::placeholder,
+input[type="email"]::placeholder,
+input[type="url"]::placeholder {
+ color: #87a6bc;
+}
+
+input[type="text"]:hover,
+input[type="tel"]:hover,
+input[type="email"]:hover,
+input[type="url"]:hover {
+ border-color: #a8bece;
+}
+
+input[type="text"]:focus,
+input[type="tel"]:focus,
+input[type="email"]:focus,
+input[type="url"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+input[type="text"]:focus::-ms-clear,
+input[type="tel"]:focus::-ms-clear,
+input[type="email"]:focus::-ms-clear,
+input[type="url"]:focus::-ms-clear {
+ display: none;
+}
+
+input[type="text"]:disabled,
+input[type="tel"]:disabled,
+input[type="email"]:disabled,
+input[type="url"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ -webkit-text-fill-color: #a8bece;
+}
+
+input[type="text"]:disabled:hover,
+input[type="tel"]:disabled:hover,
+input[type="email"]:disabled:hover,
+input[type="url"]:disabled:hover {
+ cursor: default;
+}
+
+input[type="text"]:disabled::placeholder,
+input[type="tel"]:disabled::placeholder,
+input[type="email"]:disabled::placeholder,
+input[type="url"]:disabled::placeholder {
+ color: #a8bece;
+}
+
+
+/* ==========================================================================
+** Textareas
+** ======================================================================= */
+
+textarea {
+ border-radius: 0;
+ appearance: none;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 7px 14px;
+ height: 92px;
+ width: 100%;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 1.5;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-shadow: none;
+}
+
+textarea::placeholder {
+ color: #87a6bc;
+}
+
+textarea:hover {
+ border-color: #a8bece;
+}
+
+textarea:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+textarea:focus::-ms-clear {
+ display: none;
+}
+
+textarea:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ -webkit-text-fill-color: #a8bece;
+}
+
+textarea:disabled:hover {
+ cursor: default;
+}
+
+textarea:disabled::placeholder {
+ color: #a8bece;
+}
+
+
+/* ==========================================================================
+** Checkboxes
+** ======================================================================= */
+
+.checkbox,
+input[type="checkbox"] {
+ -webkit-appearance: none;
+ display: inline-block;
+ box-sizing: border-box;
+ margin: 2px 0 0;
+ padding: 7px 14px;
+ width: 16px;
+ height: 16px;
+ float: left;
+ outline: 0;
+ padding: 0;
+ box-shadow: none;
+ background-color: #fff;
+ border: 1px solid #c8d7e1;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 0;
+ text-align: center;
+ vertical-align: middle;
+ appearance: none;
+ transition: all .15s ease-in-out;
+ clear: none;
+ cursor: pointer;
+}
+
+.checkbox:checked:before,
+input[type="checkbox"]:checked:before {
+ content: '\f147';
+ font-family: Dashicons;
+ margin: -3px 0 0 -4px;
+ float: left;
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ font-size: 20px;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ speak: none;
+ color: #00aadc;
+}
+
+.checkbox:disabled:checked:before,
+input[type="checkbox"]:disabled:checked:before {
+ color: #a8bece;
+}
+
+.checkbox:hover,
+input[type="checkbox"]:hover {
+ border-color: #a8bece;
+}
+
+.checkbox:focus,
+input[type="checkbox"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+.checkbox:disabled,
+input[type="checkbox"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ opacity: 1;
+}
+
+.checkbox:disabled:hover,
+input[type="checkbox"]:disabled:hover {
+ cursor: default;
+}
+
+.checkbox + span,
+input[type="checkbox"] + span {
+ display: block;
+ font-weight: normal;
+ margin-left: 24px;
+}
+
+
+/* ==========================================================================
+** Radio buttons
+** ======================================================================== */
+
+.radio-button,
+input[type=radio] {
+ color: #2e4453;
+ font-size: 16px;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-sizing: border-box;
+ -webkit-appearance: none;
+ clear: none;
+ cursor: pointer;
+ display: inline-block;
+ line-height: 0;
+ height: 16px;
+ margin: 2px 4px 0 0;
+ float: left;
+ outline: 0;
+ padding: 0;
+ text-align: center;
+ vertical-align: middle;
+ width: 16px;
+ min-width: 16px;
+ appearance: none;
+ border-radius: 50%;
+ line-height: 10px;
+}
+
+.radio-button:hover,
+input[type="radio"]:hover {
+ border-color: #a8bece;
+}
+
+.radio-button:focus,
+input[type="radio"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+.radio-button:focus::-ms-clear,
+input[type="radio"]:focus::-ms-clear {
+ display: none;
+}
+
+.radio-button:checked:before,
+input[type="radio"]:checked:before {
+ float: left;
+ display: inline-block;
+ content: '\2022';
+ margin: 3px;
+ width: 8px;
+ height: 8px;
+ text-indent: -9999px;
+ background: #00aadc;
+ vertical-align: middle;
+ border-radius: 50%;
+ animation: grow .2s ease-in-out;
+}
+
+.radio-button:disabled,
+input[type="radio"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ opacity: 1;
+ -webkit-text-fill-color: #a8bece;
+}
+
+.radio-button:disabled:hover,
+input[type="radio"]:disabled:hover {
+ cursor: default;
+}
+
+.radio-button:disabled::placeholder,
+input[type="radio"]:disabled::placeholder {
+ color: #a8bece;
+}
+
+.radio-button:disabled:checked::before,
+input[type="radio"]:disabled:checked:before {
+ background: #e9eff3;
+}
+
+.radio-button + span,
+input[type="radio"] + span {
+ display: block;
+ font-weight: normal;
+ margin-left: 24px;
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale(0.3);
+ }
+
+ 60% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale(0.3);
+ }
+
+ 60% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+
+/* ==========================================================================
+** Selects
+** ======================================================================== */
+
+select {
+ background: #fff url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjQzhEN0UxIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat right 10px center;
+ border-color: #c8d7e1;
+ border-style: solid;
+ border-radius: 4px;
+ border-width: 1px 1px 2px;
+ color: #2e4453;
+ cursor: pointer;
+ display: inline-block;
+ margin: 0;
+ outline: 0;
+ overflow: hidden;
+ font-size: 14px;
+ line-height: 21px;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ white-space: nowrap;
+ box-sizing: border-box;
+ /* Aligns the text to the 8px baseline grid and adds padding on right to allow for the arrow. */
+ padding: 7px 32px 9px 14px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+select:hover {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjYThiZWNlIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==);
+}
+
+select:focus {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiA8dGl0bGU+YXJyb3ctZG93bjwvdGl0bGU+IDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiA8ZGVmcz48L2RlZnM+IDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPiA8ZyBpZD0iYXJyb3ctZG93biIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgZmlsbD0iIzJlNDQ1MyI+IDxwYXRoIGQ9Ik0xNS41LDYgTDE3LDcuNSBMMTAuMjUsMTQuMjUgTDMuNSw3LjUgTDUsNiBMMTAuMjUsMTEuMjUgTDE1LjUsNiBaIiBpZD0iRG93bi1BcnJvdyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPiA8L2c+IDwvZz48L3N2Zz4=);
+ border-color: #00aadc;
+ box-shadow: 0 0 0 2px #78dcfa;
+ outline: 0;
+ -moz-outline:none;
+ -moz-user-focus:ignore;
+}
+
+select:disabled,
+select:hover:disabled {
+ background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjZTllZmYzIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat right 10px center;;
+}
+
+select.is-compact {
+ min-width: 0;
+ padding: 0 20px 2px 6px;
+ margin: 0 4px;
+ background-position: right 5px center;
+ background-size: 12px 12px;
+}
+
+/* Make it display:block when it follows a label */
+label select,
+label + select {
+ display: block;
+ min-width: 200px;
+}
+
+label select.is-compact,
+label + select.is-compact {
+ display: inline-block;
+ min-width: 0;
+}
+
+/* IE: Remove the default arrow */
+select::-ms-expand {
+ display: none;
+}
+
+/* IE: Remove default background and color styles on focus */
+select::-ms-value {
+ background: none;
+ color: #2e4453;
+}
+
+/* Firefox: Remove the focus outline, see http://stackoverflow.com/questions/3773430/remove-outline-from-select-box-in-ff/18853002#18853002 */
+select:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #2e4453;
+}
+
+
+/* ==========================================================================
+** Buttons
+** ======================================================================== */
+
+input[type="submit"] {
+ padding: 0;
+ font-size: 14px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ vertical-align: baseline;
+ background: white;
+ border-color: #c8d7e1;
+ border-style: solid;
+ border-width: 1px 1px 2px;
+ color: #2e4453;
+ cursor: pointer;
+ display: inline-block;
+ margin: 24px 0 0;
+ outline: 0;
+ overflow: hidden;
+ font-weight: 500;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ box-sizing: border-box;
+ font-size: 14px;
+ line-height: 21px;
+ border-radius: 4px;
+ padding: 7px 14px 9px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+input[type="submit"]:hover {
+ border-color: #a8bece;
+ color: #2e4453;
+}
+
+input[type="submit"]:active {
+ border-width: 2px 1px 1px;
+}
+
+input[type="submit"]:visited {
+ color: #2e4453;
+}
+
+input[type="submit"][disabled],
+input[type="submit"]:disabled {
+ color: #e9eff3;
+ background: white;
+ border-color: #e9eff3;
+ cursor: default;
+}
+
+input[type="submit"][disabled]:active,
+input[type="submit"]:disabled:active {
+ border-width: 1px 1px 2px;
+}
+
+input[type="submit"]:focus {
+ border-color: #00aadc;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+input[type="submit"].hidden {
+ display: none;
+}
+
+input[type="submit"] .gridicon {
+ position: relative;
+ top: 4px;
+ margin-top: -2px;
+ width: 18px;
+ height: 18px;
+}
+
+input[type="submit"].button-primary {
+ background: #00aadc;
+ border-color: #008ab3;
+ color: white;
+}
+
+input[type="submit"].button-primary:hover,
+input[type="submit"].button-primary:focus {
+ border-color: #005082;
+ color: white;
+}
+
+input[type="submit"].button-primary[disabled],
+input[type="submit"].button-primary:disabled {
+ background: #bceefd;
+ border-color: #8cc9e2;
+ color: white;
+}
+
+input[type="submit"].button-primary {
+ color: white;
+}
+
+
+/* ==========================================================================
+** Inline editor styles
+** ======================================================================== */
+
+
+.ui-sortable-handle {
+ cursor: move;
+}
+
+.grunion-section-header {
+ font-size: 21px;
+ margin-top: 32px;
+ font-weight: 600;
+}
+
+.grunion-form-settings:hover {
+ box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
+}
+
+.grunion-section-header:first-child {
+ margin-top: 0;
+}
+
+.grunion-type-options {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.grunion-type {
+ flex-grow: 0;
+ flex-shrink: 0;
+}
+
+.grunion-type select {
+ -webkit-appearance: none;
+ width: 100%;
+}
+
+.grunion-required {
+ padding: 27px 0 0 16px;
+ flex-grow: 0;
+ flex-shrink: 0;
+}
+
+.grunion-options {
+ padding-top: 16px;
+}
+
+.grunion-options ol {
+ list-style: none;
+ padding: 0;
+ margin: 8px 0 0;
+}
+
+.grunion-options li {
+ display: flex;
+ margin-bottom: 16px;
+}
+
+.grunion-field-edit .grunion-options {
+ display: none;
+}
+
+.delete-option,
+.delete-field {
+ color: #0087be;
+ text-decoration: none;
+ width: 40px;
+ line-height: 40px;
+ font-size: 21px;
+ text-align: center;
+ font-weight: 600;
+}
+
+.delete-field {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+
+.grunion-controls {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.grunion-update-controls {
+ text-align: right;
+ flex-grow: 1;
+}
+
+#add-field {
+ flex-grow: 0;
+}
+
+.delete-option:before,
+.delete-field:before {
+ font-family: Dashicons;
+/* content: "\f158"; /* This is the bolder X */
+ content: "\f335"; /* This is the thinner X */
+ display: inline-block;
+ speak: none;
+}
+
+.grunion-field-edit.grunion-field-checkbox-multiple .grunion-options,
+.grunion-field-edit.grunion-field-radio .grunion-options,
+.grunion-field-edit.grunion-field-select .grunion-options {
+ display: block;
+}
+
+.screen-reader-text {
+ position: absolute;
+ margin: -1px;
+ padding: 0;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ border: 0;
+ word-wrap: normal !important; /* many screen reader and browser combinations announce broken words as they would appear visually */
+}
diff --git a/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style.min.css b/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style.min.css
new file mode 100644
index 00000000..7d3a0ca0
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-inline-editing-style.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+html{direction:ltr}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;line-height:1.4em;margin:0}a,a:visited{color:#0087be;text-decoration:none}a:active,a:focus,a:hover{color:$link-highlight}.card,body{display:block;position:relative;margin:0 auto 10px auto;padding:16px;box-sizing:border-box;background:#fff;box-shadow:0 0 0 1px rgba(200,215,225,.5),0 1px 2px #e9eff3}body{margin:0;background:#f5f5f5}.card:after{content:".";display:block;height:0;clear:both;visibility:hidden}.card:focus,.card:hover{box-shadow:0 0 0 1px #999,0 1px 2px #e9eff3}.card .delete-field{display:block;float:right}@media (min-width:481px){.card{margin-bottom:16px;padding:24px}body{padding:24px}}.card.is-compact{margin-bottom:1px}@media (min-width:481px){.card.is-compact{margin-bottom:1px;padding:16px 24px}}.card>div{margin-top:24px}.card>div:first-child{margin-top:0}label{display:block;font-size:14px;font-weight:600;margin-bottom:5px;margin-top:8px}label:first-of-type{margin-top:4px}input[type=email],input[type=tel],input[type=text],input[type=url]{border-radius:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0;padding:7px 14px;width:100%;color:#2e4453;font-size:16px;line-height:1.5;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-shadow:none}input[type=email]:-ms-input-placeholder,input[type=tel]:-ms-input-placeholder,input[type=text]:-ms-input-placeholder,input[type=url]:-ms-input-placeholder{color:#87a6bc}input[type=email]::-ms-input-placeholder,input[type=tel]::-ms-input-placeholder,input[type=text]::-ms-input-placeholder,input[type=url]::-ms-input-placeholder{color:#87a6bc}input[type=email]::placeholder,input[type=tel]::placeholder,input[type=text]::placeholder,input[type=url]::placeholder{color:#87a6bc}input[type=email]:hover,input[type=tel]:hover,input[type=text]:hover,input[type=url]:hover{border-color:#a8bece}input[type=email]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}input[type=email]:focus::-ms-clear,input[type=tel]:focus::-ms-clear,input[type=text]:focus::-ms-clear,input[type=url]:focus::-ms-clear{display:none}input[type=email]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=url]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;-webkit-text-fill-color:#a8bece}input[type=email]:disabled:hover,input[type=tel]:disabled:hover,input[type=text]:disabled:hover,input[type=url]:disabled:hover{cursor:default}input[type=email]:disabled:-ms-input-placeholder,input[type=tel]:disabled:-ms-input-placeholder,input[type=text]:disabled:-ms-input-placeholder,input[type=url]:disabled:-ms-input-placeholder{color:#a8bece}input[type=email]:disabled::-ms-input-placeholder,input[type=tel]:disabled::-ms-input-placeholder,input[type=text]:disabled::-ms-input-placeholder,input[type=url]:disabled::-ms-input-placeholder{color:#a8bece}input[type=email]:disabled::placeholder,input[type=tel]:disabled::placeholder,input[type=text]:disabled::placeholder,input[type=url]:disabled::placeholder{color:#a8bece}textarea{border-radius:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0;padding:7px 14px;height:92px;width:100%;color:#2e4453;font-size:16px;line-height:1.5;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-shadow:none}textarea:-ms-input-placeholder{color:#87a6bc}textarea::-ms-input-placeholder{color:#87a6bc}textarea::placeholder{color:#87a6bc}textarea:hover{border-color:#a8bece}textarea:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}textarea:focus::-ms-clear{display:none}textarea:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;-webkit-text-fill-color:#a8bece}textarea:disabled:hover{cursor:default}textarea:disabled:-ms-input-placeholder{color:#a8bece}textarea:disabled::-ms-input-placeholder{color:#a8bece}textarea:disabled::placeholder{color:#a8bece}.checkbox,input[type=checkbox]{-webkit-appearance:none;display:inline-block;box-sizing:border-box;margin:2px 0 0;padding:7px 14px;width:16px;height:16px;float:left;outline:0;padding:0;box-shadow:none;background-color:#fff;border:1px solid #c8d7e1;color:#2e4453;font-size:16px;line-height:0;text-align:center;vertical-align:middle;-moz-appearance:none;appearance:none;transition:all .15s ease-in-out;clear:none;cursor:pointer}.checkbox:checked:before,input[type=checkbox]:checked:before{content:'\f147';font-family:Dashicons;margin:-3px 0 0 -4px;float:left;display:inline-block;vertical-align:middle;width:16px;font-size:20px;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:none;color:#00aadc}.checkbox:disabled:checked:before,input[type=checkbox]:disabled:checked:before{color:#a8bece}.checkbox:hover,input[type=checkbox]:hover{border-color:#a8bece}.checkbox:focus,input[type=checkbox]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}.checkbox:disabled,input[type=checkbox]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;opacity:1}.checkbox:disabled:hover,input[type=checkbox]:disabled:hover{cursor:default}.checkbox+span,input[type=checkbox]+span{display:block;font-weight:400;margin-left:24px}.radio-button,input[type=radio]{color:#2e4453;font-size:16px;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-sizing:border-box;-webkit-appearance:none;clear:none;cursor:pointer;display:inline-block;line-height:0;height:16px;margin:2px 4px 0 0;float:left;outline:0;padding:0;text-align:center;vertical-align:middle;width:16px;min-width:16px;-moz-appearance:none;appearance:none;border-radius:50%;line-height:10px}.radio-button:hover,input[type=radio]:hover{border-color:#a8bece}.radio-button:focus,input[type=radio]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}.radio-button:focus::-ms-clear,input[type=radio]:focus::-ms-clear{display:none}.radio-button:checked:before,input[type=radio]:checked:before{float:left;display:inline-block;content:'\2022';margin:3px;width:8px;height:8px;text-indent:-9999px;background:#00aadc;vertical-align:middle;border-radius:50%;animation:grow .2s ease-in-out}.radio-button:disabled,input[type=radio]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;opacity:1;-webkit-text-fill-color:#a8bece}.radio-button:disabled:hover,input[type=radio]:disabled:hover{cursor:default}.radio-button:disabled:-ms-input-placeholder,input[type=radio]:disabled:-ms-input-placeholder{color:#a8bece}.radio-button:disabled::-ms-input-placeholder,input[type=radio]:disabled::-ms-input-placeholder{color:#a8bece}.radio-button:disabled::placeholder,input[type=radio]:disabled::placeholder{color:#a8bece}.radio-button:disabled:checked::before,input[type=radio]:disabled:checked:before{background:#e9eff3}.radio-button+span,input[type=radio]+span{display:block;font-weight:400;margin-left:24px}@keyframes grow{0%{transform:scale(.3)}60%{transform:scale(1.15)}100%{transform:scale(1)}}@keyframes grow{0%{transform:scale(.3)}60%{transform:scale(1.15)}100%{transform:scale(1)}}select{background:#fff url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjQzhEN0UxIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat right 10px center;border-color:#c8d7e1;border-style:solid;border-radius:4px;border-width:1px 1px 2px;color:#2e4453;cursor:pointer;display:inline-block;margin:0;outline:0;overflow:hidden;font-size:14px;line-height:21px;font-weight:600;text-overflow:ellipsis;text-decoration:none;vertical-align:top;white-space:nowrap;box-sizing:border-box;padding:7px 32px 9px 14px;-webkit-appearance:none;-moz-appearance:none;appearance:none}select:hover{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjYThiZWNlIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==)}select:focus{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiA8dGl0bGU+YXJyb3ctZG93bjwvdGl0bGU+IDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiA8ZGVmcz48L2RlZnM+IDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPiA8ZyBpZD0iYXJyb3ctZG93biIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgZmlsbD0iIzJlNDQ1MyI+IDxwYXRoIGQ9Ik0xNS41LDYgTDE3LDcuNSBMMTAuMjUsMTQuMjUgTDMuNSw3LjUgTDUsNiBMMTAuMjUsMTEuMjUgTDE1LjUsNiBaIiBpZD0iRG93bi1BcnJvdyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPiA8L2c+IDwvZz48L3N2Zz4=);border-color:#00aadc;box-shadow:0 0 0 2px #78dcfa;outline:0;-moz-outline:none;-moz-user-focus:ignore}select:disabled,select:hover:disabled{background:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjZTllZmYzIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat right 10px center}select.is-compact{min-width:0;padding:0 20px 2px 6px;margin:0 4px;background-position:right 5px center;background-size:12px 12px}label select,label+select{display:block;min-width:200px}label select.is-compact,label+select.is-compact{display:inline-block;min-width:0}select::-ms-expand{display:none}select::-ms-value{background:0 0;color:#2e4453}select:-moz-focusring{color:transparent;text-shadow:0 0 0 #2e4453}input[type=submit]{padding:0;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;vertical-align:baseline;background:#fff;border-color:#c8d7e1;border-style:solid;border-width:1px 1px 2px;color:#2e4453;cursor:pointer;display:inline-block;margin:24px 0 0;outline:0;overflow:hidden;font-weight:500;text-overflow:ellipsis;text-decoration:none;vertical-align:top;box-sizing:border-box;font-size:14px;line-height:21px;border-radius:4px;padding:7px 14px 9px;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=submit]:hover{border-color:#a8bece;color:#2e4453}input[type=submit]:active{border-width:2px 1px 1px}input[type=submit]:visited{color:#2e4453}input[type=submit]:disabled,input[type=submit][disabled]{color:#e9eff3;background:#fff;border-color:#e9eff3;cursor:default}input[type=submit]:disabled:active,input[type=submit][disabled]:active{border-width:1px 1px 2px}input[type=submit]:focus{border-color:#00aadc;box-shadow:0 0 0 2px #78dcfa}input[type=submit].hidden{display:none}input[type=submit] .gridicon{position:relative;top:4px;margin-top:-2px;width:18px;height:18px}input[type=submit].button-primary{background:#00aadc;border-color:#008ab3;color:#fff}input[type=submit].button-primary:focus,input[type=submit].button-primary:hover{border-color:#005082;color:#fff}input[type=submit].button-primary:disabled,input[type=submit].button-primary[disabled]{background:#bceefd;border-color:#8cc9e2;color:#fff}input[type=submit].button-primary{color:#fff}.ui-sortable-handle{cursor:move}.grunion-section-header{font-size:21px;margin-top:32px;font-weight:600}.grunion-form-settings:hover{box-shadow:0 0 0 1px rgba(200,215,225,.5),0 1px 2px #e9eff3}.grunion-section-header:first-child{margin-top:0}.grunion-type-options{display:flex;flex-wrap:wrap}.grunion-type{flex-grow:0;flex-shrink:0}.grunion-type select{-webkit-appearance:none;width:100%}.grunion-required{padding:27px 0 0 16px;flex-grow:0;flex-shrink:0}.grunion-options{padding-top:16px}.grunion-options ol{list-style:none;padding:0;margin:8px 0 0}.grunion-options li{display:flex;margin-bottom:16px}.grunion-field-edit .grunion-options{display:none}.delete-field,.delete-option{color:#0087be;text-decoration:none;width:40px;line-height:40px;font-size:21px;text-align:center;font-weight:600}.delete-field{position:absolute;top:0;right:0}.grunion-controls{display:flex;flex-wrap:wrap}.grunion-update-controls{text-align:right;flex-grow:1}#add-field{flex-grow:0}.delete-field:before,.delete-option:before{font-family:Dashicons;content:"\f335";display:inline-block;speak:none}.grunion-field-edit.grunion-field-checkbox-multiple .grunion-options,.grunion-field-edit.grunion-field-radio .grunion-options,.grunion-field-edit.grunion-field-select .grunion-options{display:block}.screen-reader-text{position:absolute;margin:-1px;padding:0;height:1px;width:1px;overflow:hidden;clip:rect(0 0 0 0);border:0;word-wrap:normal!important} \ No newline at end of file
diff --git a/plugins/jetpack/modules/contact-form/css/editor-style-rtl.css b/plugins/jetpack/modules/contact-form/css/editor-style-rtl.css
new file mode 100644
index 00000000..e6b43f3c
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-style-rtl.css
@@ -0,0 +1,613 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+/* ==========================================================================
+** Normalize
+** ======================================================================== */
+
+body,
+label {
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
+ font-size: 16px;
+ line-height: 1.4em;
+}
+
+/* ==========================================================================
+** Card
+** ======================================================================= */
+
+.card {
+ display: block;
+ position: relative;
+ margin: 0 auto;
+ padding: 16px;
+ box-sizing: border-box;
+ background: white;
+ box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
+}
+
+.card:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+@media ( min-width: 481px ) {
+ .card {
+ padding: 24px;
+ }
+}
+
+.card > div {
+ margin-top: 24px;
+}
+
+.card > div:first-child {
+ margin-top: 0;
+}
+
+
+/* ==========================================================================
+** Labels
+** ======================================================================= */
+
+label {
+ display: block;
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 5px;
+}
+
+
+/* ==========================================================================
+** Text Inputs
+** ======================================================================= */
+
+input[type="text"],
+input[type="tel"],
+input[type="email"],
+input[type="url"] {
+ border-radius: 0;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 7px 14px;
+ width: 100%;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 1.5;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-shadow: none;
+}
+
+input[type="text"]:-ms-input-placeholder,
+input[type="tel"]:-ms-input-placeholder,
+input[type="email"]:-ms-input-placeholder,
+input[type="url"]:-ms-input-placeholder {
+ color: #87a6bc;
+}
+
+input[type="text"]::-ms-input-placeholder,
+input[type="tel"]::-ms-input-placeholder,
+input[type="email"]::-ms-input-placeholder,
+input[type="url"]::-ms-input-placeholder {
+ color: #87a6bc;
+}
+
+input[type="text"]::placeholder,
+input[type="tel"]::placeholder,
+input[type="email"]::placeholder,
+input[type="url"]::placeholder {
+ color: #87a6bc;
+}
+
+input[type="text"]:hover,
+input[type="tel"]:hover,
+input[type="email"]:hover,
+input[type="url"]:hover {
+ border-color: #a8bece;
+}
+
+input[type="text"]:focus,
+input[type="tel"]:focus,
+input[type="email"]:focus,
+input[type="url"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+input[type="text"]:focus::-ms-clear,
+input[type="tel"]:focus::-ms-clear,
+input[type="email"]:focus::-ms-clear,
+input[type="url"]:focus::-ms-clear {
+ display: none;
+}
+
+input[type="text"]:disabled,
+input[type="tel"]:disabled,
+input[type="email"]:disabled,
+input[type="url"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ -webkit-text-fill-color: #a8bece;
+}
+
+input[type="text"]:disabled:hover,
+input[type="tel"]:disabled:hover,
+input[type="email"]:disabled:hover,
+input[type="url"]:disabled:hover {
+ cursor: default;
+}
+
+input[type="text"]:disabled:-ms-input-placeholder,
+input[type="tel"]:disabled:-ms-input-placeholder,
+input[type="email"]:disabled:-ms-input-placeholder,
+input[type="url"]:disabled:-ms-input-placeholder {
+ color: #a8bece;
+}
+
+input[type="text"]:disabled::-ms-input-placeholder,
+input[type="tel"]:disabled::-ms-input-placeholder,
+input[type="email"]:disabled::-ms-input-placeholder,
+input[type="url"]:disabled::-ms-input-placeholder {
+ color: #a8bece;
+}
+
+input[type="text"]:disabled::placeholder,
+input[type="tel"]:disabled::placeholder,
+input[type="email"]:disabled::placeholder,
+input[type="url"]:disabled::placeholder {
+ color: #a8bece;
+}
+
+
+/* ==========================================================================
+** Textareas
+** ======================================================================= */
+
+textarea {
+ border-radius: 0;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 7px 14px;
+ height: 92px;
+ width: 100%;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 1.5;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-shadow: none;
+}
+
+textarea:-ms-input-placeholder {
+ color: #87a6bc;
+}
+
+textarea::-ms-input-placeholder {
+ color: #87a6bc;
+}
+
+textarea::placeholder {
+ color: #87a6bc;
+}
+
+textarea:hover {
+ border-color: #a8bece;
+}
+
+textarea:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+textarea:focus::-ms-clear {
+ display: none;
+}
+
+textarea:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ -webkit-text-fill-color: #a8bece;
+}
+
+textarea:disabled:hover {
+ cursor: default;
+}
+
+textarea:disabled:-ms-input-placeholder {
+ color: #a8bece;
+}
+
+textarea:disabled::-ms-input-placeholder {
+ color: #a8bece;
+}
+
+textarea:disabled::placeholder {
+ color: #a8bece;
+}
+
+
+/* ==========================================================================
+** Checkboxes
+** ======================================================================= */
+
+input[type="checkbox"] {
+ -webkit-appearance: none;
+ display: inline-block;
+ box-sizing: border-box;
+ margin: 2px 0 0;
+ padding: 7px 14px;
+ width: 16px;
+ height: 16px;
+ float: right;
+ outline: 0;
+ padding: 0;
+ box-shadow: none;
+ background-color: #fff;
+ border: 1px solid #c8d7e1;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 0;
+ text-align: center;
+ vertical-align: middle;
+ -moz-appearance: none;
+ appearance: none;
+ transition: all .15s ease-in-out;
+ clear: none;
+ cursor: pointer;
+}
+
+input[type="checkbox"]:checked:before {
+ content: '\f147';
+ font-family: Dashicons;
+ margin: -3px -4px 0 0;
+ float: right;
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ font-size: 20px;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ speak: none;
+ color: #00aadc;
+}
+
+input[type="checkbox"]:disabled:checked:before {
+ color: #a8bece;
+}
+
+input[type="checkbox"]:hover {
+ border-color: #a8bece;
+}
+
+input[type="checkbox"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+input[type="checkbox"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ opacity: 1;
+}
+
+input[type="checkbox"]:disabled:hover {
+ cursor: default;
+}
+
+input[type="checkbox"] + span {
+ display: block;
+ font-weight: normal;
+ margin-right: 24px;
+}
+
+
+/* ==========================================================================
+** Radio buttons
+** ======================================================================== */
+
+input[type=radio] {
+ color: #2e4453;
+ font-size: 16px;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-sizing: border-box;
+ -webkit-appearance: none;
+ clear: none;
+ cursor: pointer;
+ display: inline-block;
+ line-height: 0;
+ height: 16px;
+ margin: 2px 0 0 4px;
+ float: right;
+ outline: 0;
+ padding: 0;
+ text-align: center;
+ vertical-align: middle;
+ width: 16px;
+ min-width: 16px;
+ -moz-appearance: none;
+ appearance: none;
+ border-radius: 50%;
+ line-height: 10px;
+}
+
+input[type="radio"]:hover {
+ border-color: #a8bece;
+}
+
+input[type="radio"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+input[type="radio"]:focus::-ms-clear {
+ display: none;
+}
+
+input[type="radio"]:checked:before {
+ float: right;
+ display: inline-block;
+ content: '\2022';
+ margin: 3px;
+ width: 8px;
+ height: 8px;
+ text-indent: -9999px;
+ background: #00aadc;
+ vertical-align: middle;
+ border-radius: 50%;
+ animation: grow .2s ease-in-out;
+}
+
+input[type="radio"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ opacity: 1;
+ -webkit-text-fill-color: #a8bece;
+}
+
+input[type="radio"]:disabled:hover {
+ cursor: default;
+}
+
+input[type="radio"]:disabled:-ms-input-placeholder {
+ color: #a8bece;
+}
+
+input[type="radio"]:disabled::-ms-input-placeholder {
+ color: #a8bece;
+}
+
+input[type="radio"]:disabled::placeholder {
+ color: #a8bece;
+}
+
+input[type="radio"]:disabled:checked:before {
+ background: #e9eff3;
+}
+
+input[type="radio"] + span {
+ display: block;
+ font-weight: normal;
+ margin-right: 24px;
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale(0.3);
+ }
+
+ 60% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale(0.3);
+ }
+
+ 60% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+
+/* ==========================================================================
+** Selects
+** ======================================================================== */
+
+select {
+ background: #fff url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjQzhEN0UxIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat left 10px center;
+ border-color: #c8d7e1;
+ border-style: solid;
+ border-radius: 4px;
+ border-width: 1px 1px 2px;
+ color: #2e4453;
+ cursor: pointer;
+ display: inline-block;
+ margin: 0;
+ outline: 0;
+ overflow: hidden;
+ font-size: 14px;
+ line-height: 21px;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ white-space: nowrap;
+ box-sizing: border-box;
+ /* Aligns the text to the 8px baseline grid and adds padding on right to allow for the arrow. */
+ padding: 7px 14px 9px 32px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+select:hover {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjYThiZWNlIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==);
+}
+
+select:focus {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiA8dGl0bGU+YXJyb3ctZG93bjwvdGl0bGU+IDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiA8ZGVmcz48L2RlZnM+IDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPiA8ZyBpZD0iYXJyb3ctZG93biIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgZmlsbD0iIzJlNDQ1MyI+IDxwYXRoIGQ9Ik0xNS41LDYgTDE3LDcuNSBMMTAuMjUsMTQuMjUgTDMuNSw3LjUgTDUsNiBMMTAuMjUsMTEuMjUgTDE1LjUsNiBaIiBpZD0iRG93bi1BcnJvdyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPiA8L2c+IDwvZz48L3N2Zz4=);
+ border-color: #00aadc;
+ box-shadow: 0 0 0 2px #78dcfa;
+ outline: 0;
+ -moz-outline:none;
+ -moz-user-focus:ignore;
+}
+
+select:disabled,
+select:hover:disabled {
+ background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjZTllZmYzIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat left 10px center;;
+}
+
+select.is-compact {
+ min-width: 0;
+ padding: 0 6px 2px 20px;
+ margin: 0 4px;
+ background-position: left 5px center;
+ background-size: 12px 12px;
+}
+
+/* Make it display:block when it follows a label */
+label select,
+label + select {
+ display: block;
+ min-width: 200px;
+}
+
+label select.is-compact,
+label + select.is-compact {
+ display: inline-block;
+ min-width: 0;
+}
+
+/* IE: Remove the default arrow */
+select::-ms-expand {
+ display: none;
+}
+
+/* IE: Remove default background and color styles on focus */
+select::-ms-value {
+ background: none;
+ color: #2e4453;
+}
+
+/* Firefox: Remove the focus outline, see http://stackoverflow.com/questions/3773430/remove-outline-from-select-box-in-ff/18853002#18853002 */
+select:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #2e4453;
+}
+
+
+/* ==========================================================================
+** Buttons
+** ======================================================================== */
+
+input[type="submit"] {
+ padding: 0;
+ font-size: 14px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ vertical-align: baseline;
+ background: white;
+ border-color: #c8d7e1;
+ border-style: solid;
+ border-width: 1px 1px 2px;
+ color: #2e4453;
+ cursor: pointer;
+ display: inline-block;
+ margin: 24px 0 0;
+ outline: 0;
+ overflow: hidden;
+ font-weight: 500;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ box-sizing: border-box;
+ font-size: 14px;
+ line-height: 21px;
+ border-radius: 4px;
+ padding: 7px 14px 9px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+input[type="submit"]:hover {
+ border-color: #a8bece;
+ color: #2e4453;
+}
+
+input[type="submit"]:active {
+ border-width: 2px 1px 1px;
+}
+
+input[type="submit"]:visited {
+ color: #2e4453;
+}
+
+input[type="submit"][disabled],
+input[type="submit"]:disabled {
+ color: #e9eff3;
+ background: white;
+ border-color: #e9eff3;
+ cursor: default;
+}
+
+input[type="submit"][disabled]:active,
+input[type="submit"]:disabled:active {
+ border-width: 1px 1px 2px;
+}
+
+input[type="submit"]:focus {
+ border-color: #00aadc;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+/* ==========================================================================
+** Preview styles
+** ======================================================================== */
+
+.wpview.wpview-wrap[data-wpview-type=contact-form] iframe.inline-edit-contact-form {
+ width: 100%;
+ min-height: 500px;
+ border: 0;
+ overflow: hidden;
+ margin-bottom: 0;
+ display: block;
+}
+
+.contact-submit.contact-submit {
+ margin-top: 0;
+ margin-bottom: 0;
+}
diff --git a/plugins/jetpack/modules/contact-form/css/editor-style-rtl.min.css b/plugins/jetpack/modules/contact-form/css/editor-style-rtl.min.css
new file mode 100644
index 00000000..4562abc2
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-style-rtl.min.css
@@ -0,0 +1 @@
+body,label{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;line-height:1.4em}.card{display:block;position:relative;margin:0 auto;padding:16px;box-sizing:border-box;background:#fff;box-shadow:0 0 0 1px rgba(200,215,225,.5),0 1px 2px #e9eff3}.card:after{content:".";display:block;height:0;clear:both;visibility:hidden}@media (min-width:481px){.card{padding:24px}}.card>div{margin-top:24px}.card>div:first-child{margin-top:0}label{display:block;font-size:14px;font-weight:600;margin-bottom:5px}input[type=email],input[type=tel],input[type=text],input[type=url]{border-radius:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0;padding:7px 14px;width:100%;color:#2e4453;font-size:16px;line-height:1.5;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-shadow:none}input[type=email]:-ms-input-placeholder,input[type=tel]:-ms-input-placeholder,input[type=text]:-ms-input-placeholder,input[type=url]:-ms-input-placeholder{color:#87a6bc}input[type=email]::-ms-input-placeholder,input[type=tel]::-ms-input-placeholder,input[type=text]::-ms-input-placeholder,input[type=url]::-ms-input-placeholder{color:#87a6bc}input[type=email]::placeholder,input[type=tel]::placeholder,input[type=text]::placeholder,input[type=url]::placeholder{color:#87a6bc}input[type=email]:hover,input[type=tel]:hover,input[type=text]:hover,input[type=url]:hover{border-color:#a8bece}input[type=email]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}input[type=email]:focus::-ms-clear,input[type=tel]:focus::-ms-clear,input[type=text]:focus::-ms-clear,input[type=url]:focus::-ms-clear{display:none}input[type=email]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=url]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;-webkit-text-fill-color:#a8bece}input[type=email]:disabled:hover,input[type=tel]:disabled:hover,input[type=text]:disabled:hover,input[type=url]:disabled:hover{cursor:default}input[type=email]:disabled:-ms-input-placeholder,input[type=tel]:disabled:-ms-input-placeholder,input[type=text]:disabled:-ms-input-placeholder,input[type=url]:disabled:-ms-input-placeholder{color:#a8bece}input[type=email]:disabled::-ms-input-placeholder,input[type=tel]:disabled::-ms-input-placeholder,input[type=text]:disabled::-ms-input-placeholder,input[type=url]:disabled::-ms-input-placeholder{color:#a8bece}input[type=email]:disabled::placeholder,input[type=tel]:disabled::placeholder,input[type=text]:disabled::placeholder,input[type=url]:disabled::placeholder{color:#a8bece}textarea{border-radius:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0;padding:7px 14px;height:92px;width:100%;color:#2e4453;font-size:16px;line-height:1.5;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-shadow:none}textarea:-ms-input-placeholder{color:#87a6bc}textarea::-ms-input-placeholder{color:#87a6bc}textarea::placeholder{color:#87a6bc}textarea:hover{border-color:#a8bece}textarea:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}textarea:focus::-ms-clear{display:none}textarea:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;-webkit-text-fill-color:#a8bece}textarea:disabled:hover{cursor:default}textarea:disabled:-ms-input-placeholder{color:#a8bece}textarea:disabled::-ms-input-placeholder{color:#a8bece}textarea:disabled::placeholder{color:#a8bece}input[type=checkbox]{-webkit-appearance:none;display:inline-block;box-sizing:border-box;margin:2px 0 0;padding:7px 14px;width:16px;height:16px;float:right;outline:0;padding:0;box-shadow:none;background-color:#fff;border:1px solid #c8d7e1;color:#2e4453;font-size:16px;line-height:0;text-align:center;vertical-align:middle;-moz-appearance:none;appearance:none;transition:all .15s ease-in-out;clear:none;cursor:pointer}input[type=checkbox]:checked:before{content:'\f147';font-family:Dashicons;margin:-3px -4px 0 0;float:right;display:inline-block;vertical-align:middle;width:16px;font-size:20px;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:none;color:#00aadc}input[type=checkbox]:disabled:checked:before{color:#a8bece}input[type=checkbox]:hover{border-color:#a8bece}input[type=checkbox]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}input[type=checkbox]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;opacity:1}input[type=checkbox]:disabled:hover{cursor:default}input[type=checkbox]+span{display:block;font-weight:400;margin-right:24px}input[type=radio]{color:#2e4453;font-size:16px;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-sizing:border-box;-webkit-appearance:none;clear:none;cursor:pointer;display:inline-block;line-height:0;height:16px;margin:2px 0 0 4px;float:right;outline:0;padding:0;text-align:center;vertical-align:middle;width:16px;min-width:16px;-moz-appearance:none;appearance:none;border-radius:50%;line-height:10px}input[type=radio]:hover{border-color:#a8bece}input[type=radio]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}input[type=radio]:focus::-ms-clear{display:none}input[type=radio]:checked:before{float:right;display:inline-block;content:'\2022';margin:3px;width:8px;height:8px;text-indent:-9999px;background:#00aadc;vertical-align:middle;border-radius:50%;animation:grow .2s ease-in-out}input[type=radio]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;opacity:1;-webkit-text-fill-color:#a8bece}input[type=radio]:disabled:hover{cursor:default}input[type=radio]:disabled:-ms-input-placeholder{color:#a8bece}input[type=radio]:disabled::-ms-input-placeholder{color:#a8bece}input[type=radio]:disabled::placeholder{color:#a8bece}input[type=radio]:disabled:checked:before{background:#e9eff3}input[type=radio]+span{display:block;font-weight:400;margin-right:24px}@keyframes grow{0%{transform:scale(.3)}60%{transform:scale(1.15)}100%{transform:scale(1)}}@keyframes grow{0%{transform:scale(.3)}60%{transform:scale(1.15)}100%{transform:scale(1)}}select{background:#fff url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjQzhEN0UxIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat left 10px center;border-color:#c8d7e1;border-style:solid;border-radius:4px;border-width:1px 1px 2px;color:#2e4453;cursor:pointer;display:inline-block;margin:0;outline:0;overflow:hidden;font-size:14px;line-height:21px;font-weight:600;text-overflow:ellipsis;text-decoration:none;vertical-align:top;white-space:nowrap;box-sizing:border-box;padding:7px 14px 9px 32px;-webkit-appearance:none;-moz-appearance:none;appearance:none}select:hover{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjYThiZWNlIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==)}select:focus{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiA8dGl0bGU+YXJyb3ctZG93bjwvdGl0bGU+IDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiA8ZGVmcz48L2RlZnM+IDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPiA8ZyBpZD0iYXJyb3ctZG93biIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgZmlsbD0iIzJlNDQ1MyI+IDxwYXRoIGQ9Ik0xNS41LDYgTDE3LDcuNSBMMTAuMjUsMTQuMjUgTDMuNSw3LjUgTDUsNiBMMTAuMjUsMTEuMjUgTDE1LjUsNiBaIiBpZD0iRG93bi1BcnJvdyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPiA8L2c+IDwvZz48L3N2Zz4=);border-color:#00aadc;box-shadow:0 0 0 2px #78dcfa;outline:0;-moz-outline:none;-moz-user-focus:ignore}select:disabled,select:hover:disabled{background:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjZTllZmYzIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat left 10px center}select.is-compact{min-width:0;padding:0 6px 2px 20px;margin:0 4px;background-position:left 5px center;background-size:12px 12px}label select,label+select{display:block;min-width:200px}label select.is-compact,label+select.is-compact{display:inline-block;min-width:0}select::-ms-expand{display:none}select::-ms-value{background:0 0;color:#2e4453}select:-moz-focusring{color:transparent;text-shadow:0 0 0 #2e4453}input[type=submit]{padding:0;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;vertical-align:baseline;background:#fff;border-color:#c8d7e1;border-style:solid;border-width:1px 1px 2px;color:#2e4453;cursor:pointer;display:inline-block;margin:24px 0 0;outline:0;overflow:hidden;font-weight:500;text-overflow:ellipsis;text-decoration:none;vertical-align:top;box-sizing:border-box;font-size:14px;line-height:21px;border-radius:4px;padding:7px 14px 9px;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=submit]:hover{border-color:#a8bece;color:#2e4453}input[type=submit]:active{border-width:2px 1px 1px}input[type=submit]:visited{color:#2e4453}input[type=submit]:disabled,input[type=submit][disabled]{color:#e9eff3;background:#fff;border-color:#e9eff3;cursor:default}input[type=submit]:disabled:active,input[type=submit][disabled]:active{border-width:1px 1px 2px}input[type=submit]:focus{border-color:#00aadc;box-shadow:0 0 0 2px #78dcfa}.wpview.wpview-wrap[data-wpview-type=contact-form] iframe.inline-edit-contact-form{width:100%;min-height:500px;border:0;overflow:hidden;margin-bottom:0;display:block}.contact-submit.contact-submit{margin-top:0;margin-bottom:0} \ No newline at end of file
diff --git a/plugins/jetpack/modules/contact-form/css/editor-style.css b/plugins/jetpack/modules/contact-form/css/editor-style.css
new file mode 100644
index 00000000..e060f69a
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-style.css
@@ -0,0 +1,554 @@
+/* ==========================================================================
+** Normalize
+** ======================================================================== */
+
+body,
+label {
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
+ font-size: 16px;
+ line-height: 1.4em;
+}
+
+/* ==========================================================================
+** Card
+** ======================================================================= */
+
+.card {
+ display: block;
+ position: relative;
+ margin: 0 auto;
+ padding: 16px;
+ box-sizing: border-box;
+ background: white;
+ box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
+}
+
+.card:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+@media ( min-width: 481px ) {
+ .card {
+ padding: 24px;
+ }
+}
+
+.card > div {
+ margin-top: 24px;
+}
+
+.card > div:first-child {
+ margin-top: 0;
+}
+
+
+/* ==========================================================================
+** Labels
+** ======================================================================= */
+
+label {
+ display: block;
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 5px;
+}
+
+
+/* ==========================================================================
+** Text Inputs
+** ======================================================================= */
+
+input[type="text"],
+input[type="tel"],
+input[type="email"],
+input[type="url"] {
+ border-radius: 0;
+ appearance: none;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 7px 14px;
+ width: 100%;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 1.5;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-shadow: none;
+}
+
+input[type="text"]::placeholder,
+input[type="tel"]::placeholder,
+input[type="email"]::placeholder,
+input[type="url"]::placeholder {
+ color: #87a6bc;
+}
+
+input[type="text"]:hover,
+input[type="tel"]:hover,
+input[type="email"]:hover,
+input[type="url"]:hover {
+ border-color: #a8bece;
+}
+
+input[type="text"]:focus,
+input[type="tel"]:focus,
+input[type="email"]:focus,
+input[type="url"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+input[type="text"]:focus::-ms-clear,
+input[type="tel"]:focus::-ms-clear,
+input[type="email"]:focus::-ms-clear,
+input[type="url"]:focus::-ms-clear {
+ display: none;
+}
+
+input[type="text"]:disabled,
+input[type="tel"]:disabled,
+input[type="email"]:disabled,
+input[type="url"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ -webkit-text-fill-color: #a8bece;
+}
+
+input[type="text"]:disabled:hover,
+input[type="tel"]:disabled:hover,
+input[type="email"]:disabled:hover,
+input[type="url"]:disabled:hover {
+ cursor: default;
+}
+
+input[type="text"]:disabled::placeholder,
+input[type="tel"]:disabled::placeholder,
+input[type="email"]:disabled::placeholder,
+input[type="url"]:disabled::placeholder {
+ color: #a8bece;
+}
+
+
+/* ==========================================================================
+** Textareas
+** ======================================================================= */
+
+textarea {
+ border-radius: 0;
+ appearance: none;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 7px 14px;
+ height: 92px;
+ width: 100%;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 1.5;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-shadow: none;
+}
+
+textarea::placeholder {
+ color: #87a6bc;
+}
+
+textarea:hover {
+ border-color: #a8bece;
+}
+
+textarea:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+textarea:focus::-ms-clear {
+ display: none;
+}
+
+textarea:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ -webkit-text-fill-color: #a8bece;
+}
+
+textarea:disabled:hover {
+ cursor: default;
+}
+
+textarea:disabled::placeholder {
+ color: #a8bece;
+}
+
+
+/* ==========================================================================
+** Checkboxes
+** ======================================================================= */
+
+input[type="checkbox"] {
+ -webkit-appearance: none;
+ display: inline-block;
+ box-sizing: border-box;
+ margin: 2px 0 0;
+ padding: 7px 14px;
+ width: 16px;
+ height: 16px;
+ float: left;
+ outline: 0;
+ padding: 0;
+ box-shadow: none;
+ background-color: #fff;
+ border: 1px solid #c8d7e1;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 0;
+ text-align: center;
+ vertical-align: middle;
+ appearance: none;
+ transition: all .15s ease-in-out;
+ clear: none;
+ cursor: pointer;
+}
+
+input[type="checkbox"]:checked:before {
+ content: '\f147';
+ font-family: Dashicons;
+ margin: -3px 0 0 -4px;
+ float: left;
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ font-size: 20px;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ speak: none;
+ color: #00aadc;
+}
+
+input[type="checkbox"]:disabled:checked:before {
+ color: #a8bece;
+}
+
+input[type="checkbox"]:hover {
+ border-color: #a8bece;
+}
+
+input[type="checkbox"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+input[type="checkbox"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ opacity: 1;
+}
+
+input[type="checkbox"]:disabled:hover {
+ cursor: default;
+}
+
+input[type="checkbox"] + span {
+ display: block;
+ font-weight: normal;
+ margin-left: 24px;
+}
+
+
+/* ==========================================================================
+** Radio buttons
+** ======================================================================== */
+
+input[type=radio] {
+ color: #2e4453;
+ font-size: 16px;
+ border: 1px solid #c8d7e1;
+ background-color: #fff;
+ transition: all .15s ease-in-out;
+ box-sizing: border-box;
+ -webkit-appearance: none;
+ clear: none;
+ cursor: pointer;
+ display: inline-block;
+ line-height: 0;
+ height: 16px;
+ margin: 2px 4px 0 0;
+ float: left;
+ outline: 0;
+ padding: 0;
+ text-align: center;
+ vertical-align: middle;
+ width: 16px;
+ min-width: 16px;
+ appearance: none;
+ border-radius: 50%;
+ line-height: 10px;
+}
+
+input[type="radio"]:hover {
+ border-color: #a8bece;
+}
+
+input[type="radio"]:focus {
+ border-color: #0087be;
+ outline: none;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+input[type="radio"]:focus::-ms-clear {
+ display: none;
+}
+
+input[type="radio"]:checked:before {
+ float: left;
+ display: inline-block;
+ content: '\2022';
+ margin: 3px;
+ width: 8px;
+ height: 8px;
+ text-indent: -9999px;
+ background: #00aadc;
+ vertical-align: middle;
+ border-radius: 50%;
+ animation: grow .2s ease-in-out;
+}
+
+input[type="radio"]:disabled {
+ background: #f3f6f8;
+ border-color: #e9eff3;
+ color: #a8bece;
+ opacity: 1;
+ -webkit-text-fill-color: #a8bece;
+}
+
+input[type="radio"]:disabled:hover {
+ cursor: default;
+}
+
+input[type="radio"]:disabled::placeholder {
+ color: #a8bece;
+}
+
+input[type="radio"]:disabled:checked:before {
+ background: #e9eff3;
+}
+
+input[type="radio"] + span {
+ display: block;
+ font-weight: normal;
+ margin-left: 24px;
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale(0.3);
+ }
+
+ 60% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale(0.3);
+ }
+
+ 60% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+
+/* ==========================================================================
+** Selects
+** ======================================================================== */
+
+select {
+ background: #fff url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjQzhEN0UxIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat right 10px center;
+ border-color: #c8d7e1;
+ border-style: solid;
+ border-radius: 4px;
+ border-width: 1px 1px 2px;
+ color: #2e4453;
+ cursor: pointer;
+ display: inline-block;
+ margin: 0;
+ outline: 0;
+ overflow: hidden;
+ font-size: 14px;
+ line-height: 21px;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ white-space: nowrap;
+ box-sizing: border-box;
+ /* Aligns the text to the 8px baseline grid and adds padding on right to allow for the arrow. */
+ padding: 7px 32px 9px 14px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+select:hover {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjYThiZWNlIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==);
+}
+
+select:focus {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiA8dGl0bGU+YXJyb3ctZG93bjwvdGl0bGU+IDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiA8ZGVmcz48L2RlZnM+IDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPiA8ZyBpZD0iYXJyb3ctZG93biIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgZmlsbD0iIzJlNDQ1MyI+IDxwYXRoIGQ9Ik0xNS41LDYgTDE3LDcuNSBMMTAuMjUsMTQuMjUgTDMuNSw3LjUgTDUsNiBMMTAuMjUsMTEuMjUgTDE1LjUsNiBaIiBpZD0iRG93bi1BcnJvdyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPiA8L2c+IDwvZz48L3N2Zz4=);
+ border-color: #00aadc;
+ box-shadow: 0 0 0 2px #78dcfa;
+ outline: 0;
+ -moz-outline:none;
+ -moz-user-focus:ignore;
+}
+
+select:disabled,
+select:hover:disabled {
+ background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjZTllZmYzIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat right 10px center;;
+}
+
+select.is-compact {
+ min-width: 0;
+ padding: 0 20px 2px 6px;
+ margin: 0 4px;
+ background-position: right 5px center;
+ background-size: 12px 12px;
+}
+
+/* Make it display:block when it follows a label */
+label select,
+label + select {
+ display: block;
+ min-width: 200px;
+}
+
+label select.is-compact,
+label + select.is-compact {
+ display: inline-block;
+ min-width: 0;
+}
+
+/* IE: Remove the default arrow */
+select::-ms-expand {
+ display: none;
+}
+
+/* IE: Remove default background and color styles on focus */
+select::-ms-value {
+ background: none;
+ color: #2e4453;
+}
+
+/* Firefox: Remove the focus outline, see http://stackoverflow.com/questions/3773430/remove-outline-from-select-box-in-ff/18853002#18853002 */
+select:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #2e4453;
+}
+
+
+/* ==========================================================================
+** Buttons
+** ======================================================================== */
+
+input[type="submit"] {
+ padding: 0;
+ font-size: 14px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ vertical-align: baseline;
+ background: white;
+ border-color: #c8d7e1;
+ border-style: solid;
+ border-width: 1px 1px 2px;
+ color: #2e4453;
+ cursor: pointer;
+ display: inline-block;
+ margin: 24px 0 0;
+ outline: 0;
+ overflow: hidden;
+ font-weight: 500;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ box-sizing: border-box;
+ font-size: 14px;
+ line-height: 21px;
+ border-radius: 4px;
+ padding: 7px 14px 9px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+input[type="submit"]:hover {
+ border-color: #a8bece;
+ color: #2e4453;
+}
+
+input[type="submit"]:active {
+ border-width: 2px 1px 1px;
+}
+
+input[type="submit"]:visited {
+ color: #2e4453;
+}
+
+input[type="submit"][disabled],
+input[type="submit"]:disabled {
+ color: #e9eff3;
+ background: white;
+ border-color: #e9eff3;
+ cursor: default;
+}
+
+input[type="submit"][disabled]:active,
+input[type="submit"]:disabled:active {
+ border-width: 1px 1px 2px;
+}
+
+input[type="submit"]:focus {
+ border-color: #00aadc;
+ box-shadow: 0 0 0 2px #78dcfa;
+}
+
+/* ==========================================================================
+** Preview styles
+** ======================================================================== */
+
+.wpview.wpview-wrap[data-wpview-type=contact-form] iframe.inline-edit-contact-form {
+ width: 100%;
+ min-height: 500px;
+ border: 0;
+ overflow: hidden;
+ margin-bottom: 0;
+ display: block;
+}
+
+.contact-submit.contact-submit {
+ margin-top: 0;
+ margin-bottom: 0;
+}
diff --git a/plugins/jetpack/modules/contact-form/css/editor-style.min.css b/plugins/jetpack/modules/contact-form/css/editor-style.min.css
new file mode 100644
index 00000000..fdef6b83
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-style.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+body,label{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;line-height:1.4em}.card{display:block;position:relative;margin:0 auto;padding:16px;box-sizing:border-box;background:#fff;box-shadow:0 0 0 1px rgba(200,215,225,.5),0 1px 2px #e9eff3}.card:after{content:".";display:block;height:0;clear:both;visibility:hidden}@media (min-width:481px){.card{padding:24px}}.card>div{margin-top:24px}.card>div:first-child{margin-top:0}label{display:block;font-size:14px;font-weight:600;margin-bottom:5px}input[type=email],input[type=tel],input[type=text],input[type=url]{border-radius:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0;padding:7px 14px;width:100%;color:#2e4453;font-size:16px;line-height:1.5;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-shadow:none}input[type=email]:-ms-input-placeholder,input[type=tel]:-ms-input-placeholder,input[type=text]:-ms-input-placeholder,input[type=url]:-ms-input-placeholder{color:#87a6bc}input[type=email]::-ms-input-placeholder,input[type=tel]::-ms-input-placeholder,input[type=text]::-ms-input-placeholder,input[type=url]::-ms-input-placeholder{color:#87a6bc}input[type=email]::placeholder,input[type=tel]::placeholder,input[type=text]::placeholder,input[type=url]::placeholder{color:#87a6bc}input[type=email]:hover,input[type=tel]:hover,input[type=text]:hover,input[type=url]:hover{border-color:#a8bece}input[type=email]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}input[type=email]:focus::-ms-clear,input[type=tel]:focus::-ms-clear,input[type=text]:focus::-ms-clear,input[type=url]:focus::-ms-clear{display:none}input[type=email]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=url]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;-webkit-text-fill-color:#a8bece}input[type=email]:disabled:hover,input[type=tel]:disabled:hover,input[type=text]:disabled:hover,input[type=url]:disabled:hover{cursor:default}input[type=email]:disabled:-ms-input-placeholder,input[type=tel]:disabled:-ms-input-placeholder,input[type=text]:disabled:-ms-input-placeholder,input[type=url]:disabled:-ms-input-placeholder{color:#a8bece}input[type=email]:disabled::-ms-input-placeholder,input[type=tel]:disabled::-ms-input-placeholder,input[type=text]:disabled::-ms-input-placeholder,input[type=url]:disabled::-ms-input-placeholder{color:#a8bece}input[type=email]:disabled::placeholder,input[type=tel]:disabled::placeholder,input[type=text]:disabled::placeholder,input[type=url]:disabled::placeholder{color:#a8bece}textarea{border-radius:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0;padding:7px 14px;height:92px;width:100%;color:#2e4453;font-size:16px;line-height:1.5;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-shadow:none}textarea:-ms-input-placeholder{color:#87a6bc}textarea::-ms-input-placeholder{color:#87a6bc}textarea::placeholder{color:#87a6bc}textarea:hover{border-color:#a8bece}textarea:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}textarea:focus::-ms-clear{display:none}textarea:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;-webkit-text-fill-color:#a8bece}textarea:disabled:hover{cursor:default}textarea:disabled:-ms-input-placeholder{color:#a8bece}textarea:disabled::-ms-input-placeholder{color:#a8bece}textarea:disabled::placeholder{color:#a8bece}input[type=checkbox]{-webkit-appearance:none;display:inline-block;box-sizing:border-box;margin:2px 0 0;padding:7px 14px;width:16px;height:16px;float:left;outline:0;padding:0;box-shadow:none;background-color:#fff;border:1px solid #c8d7e1;color:#2e4453;font-size:16px;line-height:0;text-align:center;vertical-align:middle;-moz-appearance:none;appearance:none;transition:all .15s ease-in-out;clear:none;cursor:pointer}input[type=checkbox]:checked:before{content:'\f147';font-family:Dashicons;margin:-3px 0 0 -4px;float:left;display:inline-block;vertical-align:middle;width:16px;font-size:20px;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:none;color:#00aadc}input[type=checkbox]:disabled:checked:before{color:#a8bece}input[type=checkbox]:hover{border-color:#a8bece}input[type=checkbox]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}input[type=checkbox]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;opacity:1}input[type=checkbox]:disabled:hover{cursor:default}input[type=checkbox]+span{display:block;font-weight:400;margin-left:24px}input[type=radio]{color:#2e4453;font-size:16px;border:1px solid #c8d7e1;background-color:#fff;transition:all .15s ease-in-out;box-sizing:border-box;-webkit-appearance:none;clear:none;cursor:pointer;display:inline-block;line-height:0;height:16px;margin:2px 4px 0 0;float:left;outline:0;padding:0;text-align:center;vertical-align:middle;width:16px;min-width:16px;-moz-appearance:none;appearance:none;border-radius:50%;line-height:10px}input[type=radio]:hover{border-color:#a8bece}input[type=radio]:focus{border-color:#0087be;outline:0;box-shadow:0 0 0 2px #78dcfa}input[type=radio]:focus::-ms-clear{display:none}input[type=radio]:checked:before{float:left;display:inline-block;content:'\2022';margin:3px;width:8px;height:8px;text-indent:-9999px;background:#00aadc;vertical-align:middle;border-radius:50%;animation:grow .2s ease-in-out}input[type=radio]:disabled{background:#f3f6f8;border-color:#e9eff3;color:#a8bece;opacity:1;-webkit-text-fill-color:#a8bece}input[type=radio]:disabled:hover{cursor:default}input[type=radio]:disabled:-ms-input-placeholder{color:#a8bece}input[type=radio]:disabled::-ms-input-placeholder{color:#a8bece}input[type=radio]:disabled::placeholder{color:#a8bece}input[type=radio]:disabled:checked:before{background:#e9eff3}input[type=radio]+span{display:block;font-weight:400;margin-left:24px}@keyframes grow{0%{transform:scale(.3)}60%{transform:scale(1.15)}100%{transform:scale(1)}}@keyframes grow{0%{transform:scale(.3)}60%{transform:scale(1.15)}100%{transform:scale(1)}}select{background:#fff url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjQzhEN0UxIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat right 10px center;border-color:#c8d7e1;border-style:solid;border-radius:4px;border-width:1px 1px 2px;color:#2e4453;cursor:pointer;display:inline-block;margin:0;outline:0;overflow:hidden;font-size:14px;line-height:21px;font-weight:600;text-overflow:ellipsis;text-decoration:none;vertical-align:top;white-space:nowrap;box-sizing:border-box;padding:7px 32px 9px 14px;-webkit-appearance:none;-moz-appearance:none;appearance:none}select:hover{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjYThiZWNlIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==)}select:focus{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiA8dGl0bGU+YXJyb3ctZG93bjwvdGl0bGU+IDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiA8ZGVmcz48L2RlZnM+IDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPiA8ZyBpZD0iYXJyb3ctZG93biIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgZmlsbD0iIzJlNDQ1MyI+IDxwYXRoIGQ9Ik0xNS41LDYgTDE3LDcuNSBMMTAuMjUsMTQuMjUgTDMuNSw3LjUgTDUsNiBMMTAuMjUsMTEuMjUgTDE1LjUsNiBaIiBpZD0iRG93bi1BcnJvdyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPiA8L2c+IDwvZz48L3N2Zz4=);border-color:#00aadc;box-shadow:0 0 0 2px #78dcfa;outline:0;-moz-outline:none;-moz-user-focus:ignore}select:disabled,select:hover:disabled{background:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjZTllZmYzIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat right 10px center}select.is-compact{min-width:0;padding:0 20px 2px 6px;margin:0 4px;background-position:right 5px center;background-size:12px 12px}label select,label+select{display:block;min-width:200px}label select.is-compact,label+select.is-compact{display:inline-block;min-width:0}select::-ms-expand{display:none}select::-ms-value{background:0 0;color:#2e4453}select:-moz-focusring{color:transparent;text-shadow:0 0 0 #2e4453}input[type=submit]{padding:0;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;vertical-align:baseline;background:#fff;border-color:#c8d7e1;border-style:solid;border-width:1px 1px 2px;color:#2e4453;cursor:pointer;display:inline-block;margin:24px 0 0;outline:0;overflow:hidden;font-weight:500;text-overflow:ellipsis;text-decoration:none;vertical-align:top;box-sizing:border-box;font-size:14px;line-height:21px;border-radius:4px;padding:7px 14px 9px;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=submit]:hover{border-color:#a8bece;color:#2e4453}input[type=submit]:active{border-width:2px 1px 1px}input[type=submit]:visited{color:#2e4453}input[type=submit]:disabled,input[type=submit][disabled]{color:#e9eff3;background:#fff;border-color:#e9eff3;cursor:default}input[type=submit]:disabled:active,input[type=submit][disabled]:active{border-width:1px 1px 2px}input[type=submit]:focus{border-color:#00aadc;box-shadow:0 0 0 2px #78dcfa}.wpview.wpview-wrap[data-wpview-type=contact-form] iframe.inline-edit-contact-form{width:100%;min-height:500px;border:0;overflow:hidden;margin-bottom:0;display:block}.contact-submit.contact-submit{margin-top:0;margin-bottom:0} \ No newline at end of file
diff --git a/plugins/jetpack/modules/contact-form/css/editor-ui-rtl.css b/plugins/jetpack/modules/contact-form/css/editor-ui-rtl.css
new file mode 100644
index 00000000..df4c059d
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-ui-rtl.css
@@ -0,0 +1,27 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+i.mce-i-grunion {
+ font-size: 20px;
+}
+
+i.mce-i-grunion:before,
+.jetpack-contact-form-icon:before {
+ width: 24px;
+ vertical-align: top;
+ content: '';
+ display: block;
+ height: 24px;
+ background-size: 24px;
+ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="rgb(85, 93, 102)" d="M13 7.5h5v2h-5zm0 7h5v2h-5zM19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM11 6H6v5h5V6zm-1 4H7V7h3v3zm1 3H6v5h5v-5zm-1 4H7v-3h3v3z"/></svg>');
+ margin-top: -4px;
+}
+i.mce-i-grunion:before {
+ margin-top: -2px;
+ margin-right: -2px;
+}
+
+.jetpack-contact-form-icon {
+ opacity: 0.7;
+ vertical-align: text-top;
+ display: inline-block;
+ height: 18px;
+}
diff --git a/plugins/jetpack/modules/contact-form/css/editor-ui-rtl.min.css b/plugins/jetpack/modules/contact-form/css/editor-ui-rtl.min.css
new file mode 100644
index 00000000..3d7d4a17
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-ui-rtl.min.css
@@ -0,0 +1 @@
+i.mce-i-grunion{font-size:20px}.jetpack-contact-form-icon:before,i.mce-i-grunion:before{width:24px;vertical-align:top;content:'';display:block;height:24px;background-size:24px;background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="rgb(85, 93, 102)" d="M13 7.5h5v2h-5zm0 7h5v2h-5zM19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM11 6H6v5h5V6zm-1 4H7V7h3v3zm1 3H6v5h5v-5zm-1 4H7v-3h3v3z"/></svg>');margin-top:-4px}i.mce-i-grunion:before{margin-top:-2px;margin-right:-2px}.jetpack-contact-form-icon{opacity:.7;vertical-align:text-top;display:inline-block;height:18px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/contact-form/css/editor-ui.css b/plugins/jetpack/modules/contact-form/css/editor-ui.css
new file mode 100644
index 00000000..b68f810b
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-ui.css
@@ -0,0 +1,26 @@
+i.mce-i-grunion {
+ font-size: 20px;
+}
+
+i.mce-i-grunion:before,
+.jetpack-contact-form-icon:before {
+ width: 24px;
+ vertical-align: top;
+ content: '';
+ display: block;
+ height: 24px;
+ background-size: 24px;
+ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="rgb(85, 93, 102)" d="M13 7.5h5v2h-5zm0 7h5v2h-5zM19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM11 6H6v5h5V6zm-1 4H7V7h3v3zm1 3H6v5h5v-5zm-1 4H7v-3h3v3z"/></svg>');
+ margin-top: -4px;
+}
+i.mce-i-grunion:before {
+ margin-top: -2px;
+ margin-left: -2px;
+}
+
+.jetpack-contact-form-icon {
+ opacity: 0.7;
+ vertical-align: text-top;
+ display: inline-block;
+ height: 18px;
+}
diff --git a/plugins/jetpack/modules/contact-form/css/editor-ui.min.css b/plugins/jetpack/modules/contact-form/css/editor-ui.min.css
new file mode 100644
index 00000000..f7c96dfb
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/editor-ui.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+i.mce-i-grunion{font-size:20px}.jetpack-contact-form-icon:before,i.mce-i-grunion:before{width:24px;vertical-align:top;content:'';display:block;height:24px;background-size:24px;background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="rgb(85, 93, 102)" d="M13 7.5h5v2h-5zm0 7h5v2h-5zM19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM11 6H6v5h5V6zm-1 4H7V7h3v3zm1 3H6v5h5v-5zm-1 4H7v-3h3v3z"/></svg>');margin-top:-4px}i.mce-i-grunion:before{margin-top:-2px;margin-left:-2px}.jetpack-contact-form-icon{opacity:.7;vertical-align:text-top;display:inline-block;height:18px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/contact-form/css/grunion-rtl.css b/plugins/jetpack/modules/contact-form/css/grunion-rtl.css
new file mode 100644
index 00000000..952b25fa
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/grunion-rtl.css
@@ -0,0 +1 @@
+.contact-form .clear-form{clear:both}.contact-form input:-ms-input-placeholder{transition:opacity .3s ease-out}.contact-form input::-ms-input-placeholder{transition:opacity .3s ease-out}.contact-form input::placeholder{transition:opacity .3s ease-out}.contact-form input:hover:-ms-input-placeholder{opacity:.5}.contact-form input:hover::-ms-input-placeholder{opacity:.5}.contact-form input:hover::placeholder{opacity:.5}.contact-form input:focus:-ms-input-placeholder{opacity:.3}.contact-form input:focus::-ms-input-placeholder{opacity:.3}.contact-form input:focus::placeholder{opacity:.3}.contact-form input[type=email],.contact-form input[type=text],.contact-form input[type=url]{width:300px;max-width:98%;margin-bottom:13px}.contact-form select{margin-bottom:13px}.contact-form textarea{height:200px;width:80%;float:none;margin-bottom:13px}.contact-form input[type=checkbox],.contact-form input[type=radio]{float:none;margin-bottom:13px}.contact-form label{margin-bottom:3px;float:none;font-weight:700;display:block}.contact-form label.checkbox,.contact-form label.radio{margin-bottom:3px;float:none;font-weight:700;display:inline-block}.contact-form label span{color:#aaa;margin-right:4px;font-weight:400}.contact-form-submission{margin-bottom:4em;padding:1.5em 1em}.contact-form-submission p{margin:0 auto;word-wrap:break-word}.form-errors .form-error-message{color:red}.textwidget .contact-form input[type=email],.textwidget .contact-form input[type=text],.textwidget .contact-form input[type=url],.textwidget .contact-form textarea{width:250px;max-width:100%;box-sizing:border-box}#jetpack-check-feedback-spam{margin:1px 0 0 8px}.jetpack-check-feedback-spam-spinner{display:inline-block;margin-top:7px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/contact-form/css/grunion.css b/plugins/jetpack/modules/contact-form/css/grunion.css
new file mode 100644
index 00000000..1089188d
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/grunion.css
@@ -0,0 +1,91 @@
+.contact-form .clear-form {
+ clear: both;
+}
+
+.contact-form input::placeholder {
+ transition: opacity .3s ease-out;
+}
+.contact-form input:hover::placeholder {
+ opacity: 0.5;
+}
+.contact-form input:focus::placeholder {
+ opacity: 0.3;
+}
+
+.contact-form input[type='text'],
+.contact-form input[type='email'],
+.contact-form input[type='url'] {
+ width: 300px;
+ max-width: 98%;
+ margin-bottom: 13px;
+}
+
+.contact-form select {
+ margin-bottom: 13px;
+}
+
+.contact-form textarea {
+ height: 200px;
+ width: 80%;
+ float: none;
+ margin-bottom: 13px;
+}
+
+.contact-form input[type='radio'],
+.contact-form input[type='checkbox'] {
+ float: none;
+ margin-bottom: 13px;
+}
+
+.contact-form label {
+ margin-bottom: 3px;
+ float: none;
+ font-weight: bold;
+ display: block;
+}
+
+.contact-form label.checkbox,
+.contact-form label.radio {
+ margin-bottom: 3px;
+ float: none;
+ font-weight: bold;
+ display: inline-block;
+}
+
+.contact-form label span {
+ color: #AAA;
+ margin-left: 4px;
+ font-weight: normal;
+}
+
+.contact-form-submission {
+ margin-bottom: 4em;
+ padding: 1.5em 1em;
+}
+
+.contact-form-submission p {
+ margin: 0 auto;
+ word-wrap: break-word;
+}
+
+.form-errors .form-error-message {
+ color: red;
+}
+
+.textwidget .contact-form input[type='text'],
+.textwidget .contact-form input[type='email'],
+.textwidget .contact-form input[type='url'],
+.textwidget .contact-form textarea {
+ width: 250px;
+ max-width: 100%;
+ box-sizing: border-box;
+}
+
+#jetpack-check-feedback-spam {
+ margin: 1px 8px 0px 0px;
+}
+
+.jetpack-check-feedback-spam-spinner {
+ display: inline-block;
+ margin-top: 7px;
+}
diff --git a/plugins/jetpack/modules/contact-form/css/jquery-ui-datepicker.css b/plugins/jetpack/modules/contact-form/css/jquery-ui-datepicker.css
new file mode 100644
index 00000000..e82283eb
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/css/jquery-ui-datepicker.css
@@ -0,0 +1,160 @@
+.ui-datepicker {
+ padding: 0;
+ margin: 0;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+ background-color: #fff;
+ border: 1px solid #dfdfdf;
+ border-top: none;
+ -webkit-box-shadow: 0 3px 6px rgba(0, 0, 0, 0.075);
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.075);
+ width: auto;
+}
+
+.ui-datepicker * {
+ padding: 0;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.ui-datepicker table {
+ width: auto;
+ margin: 0;
+ border: none;
+ border-collapse: collapse;
+}
+
+.ui-datepicker .ui-widget-header,
+.ui-datepicker .ui-datepicker-header {
+ background-image: none;
+ border: none;
+ font-weight: normal;
+}
+
+.ui-datepicker .ui-datepicker-header .ui-state-hover {
+ background: transparent;
+ border-color: transparent;
+ cursor: pointer;
+}
+
+.ui-datepicker .ui-datepicker-title {
+ margin: 0;
+ padding: 10px 0;
+ font-size: 14px;
+ line-height: 14px;
+ text-align: center;
+}
+
+.ui-datepicker .ui-datepicker-prev,
+.ui-datepicker .ui-datepicker-next {
+ position: relative;
+ top: 0;
+ height: 34px;
+ width: 34px;
+}
+
+.ui-datepicker .ui-state-hover.ui-datepicker-prev,
+.ui-datepicker .ui-state-hover.ui-datepicker-next {
+ border: none;
+}
+
+.ui-datepicker .ui-datepicker-prev,
+.ui-datepicker .ui-datepicker-prev-hover {
+ left: 0;
+}
+
+.ui-datepicker .ui-datepicker-next,
+.ui-datepicker .ui-datepicker-next-hover {
+ right: 0;
+}
+
+.ui-datepicker .ui-datepicker-next span,
+.ui-datepicker .ui-datepicker-prev span {
+ display: none;
+}
+
+.ui-datepicker .ui-datepicker-prev {
+ float: left;
+}
+
+.ui-datepicker .ui-datepicker-next {
+ float: right;
+}
+
+.ui-datepicker .ui-datepicker-prev:before,
+.ui-datepicker .ui-datepicker-next:before {
+ font: normal 20px/34px 'dashicons';
+ padding-left: 7px;
+ speak: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ width: 34px;
+ height: 34px;
+}
+
+.ui-datepicker .ui-datepicker-prev:before {
+ content: '\f341';
+}
+
+.ui-datepicker .ui-datepicker-next:before {
+ content: '\f345';
+}
+
+.ui-datepicker .ui-datepicker-prev-hover:before,
+.ui-datepicker .ui-datepicker-next-hover:before {
+ opacity: 0.7;
+}
+
+.ui-datepicker select.ui-datepicker-month,
+.ui-datepicker select.ui-datepicker-year {
+ width: 33%;
+}
+
+.ui-datepicker thead {
+ font-weight: 600;
+}
+
+.ui-datepicker th {
+ padding: 10px;
+ border-width: 1px;
+}
+
+.ui-datepicker td {
+ padding: 0;
+ border: 1px solid #f4f4f4;
+}
+
+.ui-datepicker td.ui-datepicker-other-month {
+ border: transparent;
+}
+
+.ui-datepicker td.ui-datepicker-week-end {
+ background-color: #f4f4f4;
+ border: 1px solid #f4f4f4;
+}
+
+.ui-datepicker td.ui-datepicker-today {
+ background-color: #f0f0c0;
+}
+
+.ui-datepicker td.ui-datepicker-current-day {
+ background: #bbdd88;
+}
+
+.ui-datepicker td .ui-state-default {
+ background: transparent;
+ border: none;
+ text-align: center;
+ text-decoration: none;
+ width: auto;
+ display: block;
+ padding: 5px 10px;
+ font-weight: normal;
+ color: #444;
+}
+
+.ui-datepicker td.ui-state-disabled .ui-state-default {
+ opacity: 0.5;
+}
diff --git a/plugins/jetpack/modules/contact-form/grunion-contact-form.php b/plugins/jetpack/modules/contact-form/grunion-contact-form.php
new file mode 100644
index 00000000..a26dc20c
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/grunion-contact-form.php
@@ -0,0 +1,3501 @@
+<?php
+
+/*
+Plugin Name: Grunion Contact Form
+Description: Add a contact form to any post, page or text widget. Emails will be sent to the post's author by default, or any email address you choose. As seen on WordPress.com.
+Plugin URI: http://automattic.com/#
+AUthor: Automattic, Inc.
+Author URI: http://automattic.com/
+Version: 2.4
+License: GPLv2 or later
+*/
+
+define( 'GRUNION_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
+define( 'GRUNION_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
+
+if ( is_admin() ) {
+ require_once GRUNION_PLUGIN_DIR . 'admin.php';
+}
+
+add_action( 'rest_api_init', 'grunion_contact_form_require_endpoint' );
+function grunion_contact_form_require_endpoint() {
+ require_once GRUNION_PLUGIN_DIR . 'class-grunion-contact-form-endpoint.php';
+}
+
+/**
+ * Sets up various actions, filters, post types, post statuses, shortcodes.
+ */
+class Grunion_Contact_Form_Plugin {
+
+ /**
+ * @var string The Widget ID of the widget currently being processed. Used to build the unique contact-form ID for forms embedded in widgets.
+ */
+ public $current_widget_id;
+
+ static $using_contact_form_field = false;
+
+ /**
+ * @var int The last Feedback Post ID Erased as part of the Personal Data Eraser.
+ * Helps with pagination.
+ */
+ private $pde_last_post_id_erased = 0;
+
+ /**
+ * @var string The email address for which we are deleting/exporting all feedbacks
+ * as part of a Personal Data Eraser or Personal Data Exporter request.
+ */
+ private $pde_email_address = '';
+
+ static function init() {
+ static $instance = false;
+
+ if ( ! $instance ) {
+ $instance = new Grunion_Contact_Form_Plugin;
+
+ // Schedule our daily cleanup
+ add_action( 'wp_scheduled_delete', array( $instance, 'daily_akismet_meta_cleanup' ) );
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Runs daily to clean up spam detection metadata after 15 days. Keeps your DB squeaky clean.
+ */
+ public function daily_akismet_meta_cleanup() {
+ global $wpdb;
+
+ $feedback_ids = $wpdb->get_col( "SELECT p.ID FROM {$wpdb->posts} as p INNER JOIN {$wpdb->postmeta} as m on m.post_id = p.ID WHERE p.post_type = 'feedback' AND m.meta_key = '_feedback_akismet_values' AND DATE_SUB(NOW(), INTERVAL 15 DAY) > p.post_date_gmt LIMIT 10000" );
+
+ if ( empty( $feedback_ids ) ) {
+ return;
+ }
+
+ /**
+ * Fires right before deleting the _feedback_akismet_values post meta on $feedback_ids
+ *
+ * @module contact-form
+ *
+ * @since 6.1.0
+ *
+ * @param array $feedback_ids list of feedback post ID
+ */
+ do_action( 'jetpack_daily_akismet_meta_cleanup_before', $feedback_ids );
+ foreach ( $feedback_ids as $feedback_id ) {
+ delete_post_meta( $feedback_id, '_feedback_akismet_values' );
+ }
+
+ /**
+ * Fires right after deleting the _feedback_akismet_values post meta on $feedback_ids
+ *
+ * @module contact-form
+ *
+ * @since 6.1.0
+ *
+ * @param array $feedback_ids list of feedback post ID
+ */
+ do_action( 'jetpack_daily_akismet_meta_cleanup_after', $feedback_ids );
+ }
+
+ /**
+ * Strips HTML tags from input. Output is NOT HTML safe.
+ *
+ * @param mixed $data_with_tags
+ * @return mixed
+ */
+ public static function strip_tags( $data_with_tags ) {
+ if ( is_array( $data_with_tags ) ) {
+ foreach ( $data_with_tags as $index => $value ) {
+ $index = sanitize_text_field( strval( $index ) );
+ $value = wp_kses( strval( $value ), array() );
+ $value = str_replace( '&amp;', '&', $value ); // undo damage done by wp_kses_normalize_entities()
+
+ $data_without_tags[ $index ] = $value;
+ }
+ } else {
+ $data_without_tags = wp_kses( $data_with_tags, array() );
+ $data_without_tags = str_replace( '&amp;', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities()
+ }
+
+ return $data_without_tags;
+ }
+
+ /**
+ * Class uses singleton pattern; use Grunion_Contact_Form_Plugin::init() to initialize.
+ */
+ protected function __construct() {
+ $this->add_shortcode();
+
+ // While generating the output of a text widget with a contact-form shortcode, we need to know its widget ID.
+ add_action( 'dynamic_sidebar', array( $this, 'track_current_widget' ) );
+
+ // Add a "widget" shortcode attribute to all contact-form shortcodes embedded in widgets
+ add_filter( 'widget_text', array( $this, 'widget_atts' ), 0 );
+
+ // If Text Widgets don't get shortcode processed, hack ours into place.
+ if (
+ version_compare( get_bloginfo( 'version' ), '4.9-z', '<=' )
+ && ! has_filter( 'widget_text', 'do_shortcode' )
+ ) {
+ add_filter( 'widget_text', array( $this, 'widget_shortcode_hack' ), 5 );
+ }
+
+ add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_blacklist' ), 10, 2 );
+
+ // Akismet to the rescue
+ if ( defined( 'AKISMET_VERSION' ) || function_exists( 'akismet_http_post' ) ) {
+ add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_akismet' ), 10, 2 );
+ add_action( 'contact_form_akismet', array( $this, 'akismet_submit' ), 10, 2 );
+ }
+
+ add_action( 'loop_start', array( 'Grunion_Contact_Form', '_style_on' ) );
+
+ add_action( 'wp_ajax_grunion-contact-form', array( $this, 'ajax_request' ) );
+ add_action( 'wp_ajax_nopriv_grunion-contact-form', array( $this, 'ajax_request' ) );
+
+ // GDPR: personal data exporter & eraser.
+ add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_personal_data_exporter' ) );
+ add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_personal_data_eraser' ) );
+
+ // Export to CSV feature
+ if ( is_admin() ) {
+ add_action( 'admin_init', array( $this, 'download_feedback_as_csv' ) );
+ add_action( 'admin_footer-edit.php', array( $this, 'export_form' ) );
+ add_action( 'admin_menu', array( $this, 'admin_menu' ) );
+ add_action( 'current_screen', array( $this, 'unread_count' ) );
+ }
+
+ // custom post type we'll use to keep copies of the feedback items
+ register_post_type(
+ 'feedback', array(
+ 'labels' => array(
+ 'name' => __( 'Feedback', 'jetpack' ),
+ 'singular_name' => __( 'Feedback', 'jetpack' ),
+ 'search_items' => __( 'Search Feedback', 'jetpack' ),
+ 'not_found' => __( 'No feedback found', 'jetpack' ),
+ 'not_found_in_trash' => __( 'No feedback found', 'jetpack' ),
+ ),
+ // Matrial Ballot icon
+ 'menu_icon' => 'data:image/svg+xml;base64,' . base64_encode('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M13 7.5h5v2h-5zm0 7h5v2h-5zM19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM11 6H6v5h5V6zm-1 4H7V7h3v3zm1 3H6v5h5v-5zm-1 4H7v-3h3v3z"/></svg>'),
+ 'show_ui' => true,
+ 'show_in_admin_bar' => false,
+ 'public' => false,
+ 'rewrite' => false,
+ 'query_var' => false,
+ 'capability_type' => 'page',
+ 'show_in_rest' => true,
+ 'rest_controller_class' => 'Grunion_Contact_Form_Endpoint',
+ 'capabilities' => array(
+ 'create_posts' => false,
+ 'publish_posts' => 'publish_pages',
+ 'edit_posts' => 'edit_pages',
+ 'edit_others_posts' => 'edit_others_pages',
+ 'delete_posts' => 'delete_pages',
+ 'delete_others_posts' => 'delete_others_pages',
+ 'read_private_posts' => 'read_private_pages',
+ 'edit_post' => 'edit_page',
+ 'delete_post' => 'delete_page',
+ 'read_post' => 'read_page',
+ ),
+ 'map_meta_cap' => true,
+ )
+ );
+
+ // Add to REST API post type whitelist
+ add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
+
+ // Add "spam" as a post status
+ register_post_status(
+ 'spam', array(
+ 'label' => 'Spam',
+ 'public' => false,
+ 'exclude_from_search' => true,
+ 'show_in_admin_all_list' => false,
+ 'label_count' => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
+ 'protected' => true,
+ '_builtin' => false,
+ )
+ );
+
+ // POST handler
+ if (
+ isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
+ &&
+ isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
+ &&
+ isset( $_POST['contact-form-id'] )
+ ) {
+ add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
+ }
+
+ /*
+ Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
+ *
+ * function remove_grunion_style() {
+ * wp_deregister_style('grunion.css');
+ * }
+ * add_action('wp_print_styles', 'remove_grunion_style');
+ */
+ wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
+ wp_style_add_data( 'grunion.css', 'rtl', 'replace' );
+
+ self::register_contact_form_blocks();
+ }
+
+ private static function register_contact_form_blocks() {
+ jetpack_register_block( 'jetpack/contact-form', array(
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_form' ),
+ ) );
+
+ // Field render methods.
+ jetpack_register_block( 'jetpack/field-text', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_text' ),
+ ) );
+ jetpack_register_block( 'jetpack/field-name', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_name' ),
+ ) );
+ jetpack_register_block( 'jetpack/field-email', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_email' ),
+ ) );
+ jetpack_register_block( 'jetpack/field-url', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_url' ),
+ ) );
+ jetpack_register_block( 'jetpack/field-date', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_date' ),
+ ) );
+ jetpack_register_block( 'jetpack/field-telephone', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_telephone' ),
+ ) );
+ jetpack_register_block( 'jetpack/field-textarea', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_textarea' ),
+ ) );
+ jetpack_register_block( 'jetpack/field-checkbox', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_checkbox' ),
+ ) );
+ jetpack_register_block( 'jetpack/field-checkbox-multiple', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_checkbox_multiple' ),
+ ) );
+ jetpack_register_block( 'jetpack/field-radio', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_radio' ),
+ ) );
+ jetpack_register_block( 'jetpack/field-select', array(
+ 'parent' => array( 'jetpack/contact-form' ),
+ 'render_callback' => array( __CLASS__, 'gutenblock_render_field_select' ),
+ ) );
+ }
+
+ public static function gutenblock_render_form( $atts, $content ) {
+ return Grunion_Contact_Form::parse( $atts, do_blocks( $content ) );
+ }
+
+ public static function block_attributes_to_shortcode_attributes( $atts, $type ) {
+ $atts['type'] = $type;
+ if ( isset( $atts['className'] ) ) {
+ $atts['class'] = $atts['className'];
+ unset( $atts['className'] );
+ }
+
+ if ( isset( $atts['defaultValue'] ) ) {
+ $atts['default'] = $atts['defaultValue'];
+ unset( $atts['defaultValue'] );
+ }
+
+ return $atts;
+ }
+
+ public static function gutenblock_render_field_text( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'text' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+ public static function gutenblock_render_field_name( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'name' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+ public static function gutenblock_render_field_email( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'email' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+ public static function gutenblock_render_field_url( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'url' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+ public static function gutenblock_render_field_date( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'date' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+ public static function gutenblock_render_field_telephone( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'telephone' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+ public static function gutenblock_render_field_textarea( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'textarea' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+ public static function gutenblock_render_field_checkbox( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'checkbox' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+ public static function gutenblock_render_field_checkbox_multiple( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'checkbox-multiple' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+ public static function gutenblock_render_field_radio( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'radio' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+ public static function gutenblock_render_field_select( $atts, $content ) {
+ $atts = self::block_attributes_to_shortcode_attributes( $atts, 'select' );
+ return Grunion_Contact_Form::parse_contact_field( $atts, $content );
+ }
+
+ /**
+ * Add the 'Export' menu item as a submenu of Feedback.
+ */
+ public function admin_menu() {
+ add_submenu_page(
+ 'edit.php?post_type=feedback',
+ __( 'Export feedback as CSV', 'jetpack' ),
+ __( 'Export CSV', 'jetpack' ),
+ 'export',
+ 'feedback-export',
+ array( $this, 'export_form' )
+ );
+ }
+
+ /**
+ * Add to REST API post type whitelist
+ */
+ function allow_feedback_rest_api_type( $post_types ) {
+ $post_types[] = 'feedback';
+ return $post_types;
+ }
+
+ /**
+ * Display the count of new feedback entries received. It's reset when user visits the Feedback screen.
+ *
+ * @since 4.1.0
+ *
+ * @param object $screen Information about the current screen.
+ */
+ function unread_count( $screen ) {
+ if ( isset( $screen->post_type ) && 'feedback' == $screen->post_type ) {
+ update_option( 'feedback_unread_count', 0 );
+ } else {
+ global $menu;
+ if ( isset( $menu ) && is_array( $menu ) && ! empty( $menu ) ) {
+ foreach ( $menu as $index => $menu_item ) {
+ if ( 'edit.php?post_type=feedback' == $menu_item[2] ) {
+ $unread = get_option( 'feedback_unread_count', 0 );
+ if ( $unread > 0 ) {
+ $unread_count = current_user_can( 'publish_pages' ) ? " <span class='feedback-unread count-{$unread} awaiting-mod'><span class='feedback-unread-count'>" . number_format_i18n( $unread ) . '</span></span>' : '';
+ $menu[ $index ][0] .= $unread_count;
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles all contact-form POST submissions
+ *
+ * Conditionally attached to `template_redirect`
+ */
+ function process_form_submission() {
+ // Add a filter to replace tokens in the subject field with sanitized field values
+ add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
+
+ $id = stripslashes( $_POST['contact-form-id'] );
+ $hash = isset( $_POST['contact-form-hash'] ) ? $_POST['contact-form-hash'] : null;
+ $hash = preg_replace( '/[^\da-f]/i', '', $hash );
+
+ if ( is_user_logged_in() ) {
+ check_admin_referer( "contact-form_{$id}" );
+ }
+
+ $is_widget = 0 === strpos( $id, 'widget-' );
+
+ $form = false;
+
+ if ( $is_widget ) {
+ // It's a form embedded in a text widget
+ $this->current_widget_id = substr( $id, 7 ); // remove "widget-"
+ $widget_type = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
+
+ // Is the widget active?
+ $sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
+
+ // This is lame - no core API for getting a widget by ID
+ $widget = isset( $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] ) ? $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] : false;
+
+ if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
+ // prevent PHP notices by populating widget args
+ $widget_args = array(
+ 'before_widget' => '',
+ 'after_widget' => '',
+ 'before_title' => '',
+ 'after_title' => '',
+ );
+ // This is lamer - no API for outputting a given widget by ID
+ ob_start();
+ // Process the widget to populate Grunion_Contact_Form::$last
+ call_user_func( $widget['callback'], $widget_args, $widget['params'][0] );
+ ob_end_clean();
+ }
+ } else {
+ // It's a form embedded in a post
+ $post = get_post( $id );
+
+ // Process the content to populate Grunion_Contact_Form::$last
+ /** This filter is already documented in core. wp-includes/post-template.php */
+ apply_filters( 'the_content', $post->post_content );
+ }
+
+ $form = isset( Grunion_Contact_Form::$forms[ $hash ] ) ? Grunion_Contact_Form::$forms[ $hash ] : null;
+
+ // No form may mean user is using do_shortcode, grab the form using the stored post meta
+ if ( ! $form ) {
+
+ // Get shortcode from post meta
+ $shortcode = get_post_meta( $_POST['contact-form-id'], "_g_feedback_shortcode_{$hash}", true );
+
+ // Format it
+ if ( $shortcode != '' ) {
+
+ // Get attributes from post meta.
+ $parameters = '';
+ $attributes = get_post_meta( $_POST['contact-form-id'], "_g_feedback_shortcode_atts_{$hash}", true );
+ if ( ! empty( $attributes ) && is_array( $attributes ) ) {
+ foreach ( array_filter( $attributes ) as $param => $value ) {
+ $parameters .= " $param=\"$value\"";
+ }
+ }
+
+ $shortcode = '[contact-form' . $parameters . ']' . $shortcode . '[/contact-form]';
+ do_shortcode( $shortcode );
+
+ // Recreate form
+ $form = Grunion_Contact_Form::$last;
+ }
+
+ if ( ! $form ) {
+ return false;
+ }
+ }
+
+ if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
+ return $form->errors;
+ }
+
+ // Process the form
+ return $form->process_submission();
+ }
+
+ function ajax_request() {
+ $submission_result = self::process_form_submission();
+
+ if ( ! $submission_result ) {
+ header( 'HTTP/1.1 500 Server Error', 500, true );
+ echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
+ esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
+ echo '</li></ul></div>';
+ } elseif ( is_wp_error( $submission_result ) ) {
+ header( 'HTTP/1.1 400 Bad Request', 403, true );
+ echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
+ echo esc_html( $submission_result->get_error_message() );
+ echo '</li></ul></div>';
+ } else {
+ echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
+ }
+
+ die;
+ }
+
+ /**
+ * Ensure the post author is always zero for contact-form feedbacks
+ * Attached to `wp_insert_post_data`
+ *
+ * @see Grunion_Contact_Form::process_submission()
+ *
+ * @param array $data the data to insert
+ * @param array $postarr the data sent to wp_insert_post()
+ * @return array The filtered $data to insert
+ */
+ function insert_feedback_filter( $data, $postarr ) {
+ if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
+ $data['post_author'] = 0;
+ }
+
+ return $data;
+ }
+ /*
+ * Adds our contact-form shortcode
+ * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
+ */
+ function add_shortcode() {
+ add_shortcode( 'contact-form', array( 'Grunion_Contact_Form', 'parse' ) );
+ add_shortcode( 'contact-field', array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
+ }
+
+ static function tokenize_label( $label ) {
+ return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
+ }
+
+ static function sanitize_value( $value ) {
+ return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
+ }
+
+ /**
+ * Replaces tokens like {city} or {City} (case insensitive) with the value
+ * of an input field of that name
+ *
+ * @param string $subject
+ * @param array $field_values Array with field label => field value associations
+ *
+ * @return string The filtered $subject with the tokens replaced
+ */
+ function replace_tokens_with_input( $subject, $field_values ) {
+ // Wrap labels into tokens (inside {})
+ $wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
+ // Sanitize all values
+ $sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
+
+ foreach ( $sanitized_values as $k => $sanitized_value ) {
+ if ( is_array( $sanitized_value ) ) {
+ $sanitized_values[ $k ] = implode( ', ', $sanitized_value );
+ }
+ }
+
+ // Search for all valid tokens (based on existing fields) and replace with the field's value
+ $subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
+ return $subject;
+ }
+
+ /**
+ * Tracks the widget currently being processed.
+ * Attached to `dynamic_sidebar`
+ *
+ * @see $current_widget_id
+ *
+ * @param array $widget The widget data
+ */
+ function track_current_widget( $widget ) {
+ $this->current_widget_id = $widget['id'];
+ }
+
+ /**
+ * Adds a "widget" attribute to every contact-form embedded in a text widget.
+ * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
+ * Attached to `widget_text`
+ *
+ * @param string $text The widget text
+ * @return string The filtered widget text
+ */
+ function widget_atts( $text ) {
+ Grunion_Contact_Form::style( true );
+
+ return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
+ }
+
+ /**
+ * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
+ * Attached to `widget_text`
+ *
+ * @param string $text The widget text
+ * @return string The contact-form filtered widget text
+ */
+ function widget_shortcode_hack( $text ) {
+ if ( ! preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
+ return $text;
+ }
+
+ $old = $GLOBALS['shortcode_tags'];
+ remove_all_shortcodes();
+ Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
+ $this->add_shortcode();
+
+ $text = do_shortcode( $text );
+
+ Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
+ $GLOBALS['shortcode_tags'] = $old;
+
+ return $text;
+ }
+
+ /**
+ * Check if a submission matches the Comment Blacklist.
+ * The Comment Blacklist is a means to moderate discussion, and contact
+ * forms are 1:1 discussion forums, ripe for abuse by users who are being
+ * removed from the public discussion.
+ * Attached to `jetpack_contact_form_is_spam`
+ *
+ * @param bool $is_spam
+ * @param array $form
+ * @return bool TRUE => spam, FALSE => not spam
+ */
+ function is_spam_blacklist( $is_spam, $form = array() ) {
+ if ( $is_spam ) {
+ return $is_spam;
+ }
+
+ if ( wp_blacklist_check( $form['comment_author'], $form['comment_author_email'], $form['comment_author_url'], $form['comment_content'], $form['user_ip'], $form['user_agent'] ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
+ * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
+ *
+ * @param array $form Contact form feedback array
+ * @return array feedback array with additional data ready for submission to Akismet
+ */
+ function prepare_for_akismet( $form ) {
+ $form['comment_type'] = 'contact_form';
+ $form['user_ip'] = $_SERVER['REMOTE_ADDR'];
+ $form['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
+ $form['referrer'] = $_SERVER['HTTP_REFERER'];
+ $form['blog'] = get_option( 'home' );
+
+ foreach ( $_SERVER as $key => $value ) {
+ if ( ! is_string( $value ) ) {
+ continue;
+ }
+ if ( in_array( $key, array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'HTTP_USER_AGENT', 'HTTP_REFERER' ) ) ) {
+ // We don't care about cookies, and the UA and Referrer were caught above.
+ continue;
+ } elseif ( in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ) ) ) {
+ // All three of these are relevant indicators and should be passed along.
+ $form[ $key ] = $value;
+ } elseif ( wp_startswith( $key, 'HTTP_' ) ) {
+ // Any other HTTP header indicators.
+ // `wp_startswith()` is a wpcom helper function and is included in Jetpack via `functions.compat.php`
+ $form[ $key ] = $value;
+ }
+ }
+
+ return $form;
+ }
+
+ /**
+ * Submit contact-form data to Akismet to check for spam.
+ * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
+ * Attached to `jetpack_contact_form_is_spam`
+ *
+ * @param bool $is_spam
+ * @param array $form
+ * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
+ */
+ function is_spam_akismet( $is_spam, $form = array() ) {
+ global $akismet_api_host, $akismet_api_port;
+
+ // The signature of this function changed from accepting just $form.
+ // If something only sends an array, assume it's still using the old
+ // signature and work around it.
+ if ( empty( $form ) && is_array( $is_spam ) ) {
+ $form = $is_spam;
+ $is_spam = false;
+ }
+
+ // If a previous filter has alrady marked this as spam, trust that and move on.
+ if ( $is_spam ) {
+ return $is_spam;
+ }
+
+ if ( ! function_exists( 'akismet_http_post' ) && ! defined( 'AKISMET_VERSION' ) ) {
+ return false;
+ }
+
+ $query_string = http_build_query( $form );
+
+ if ( method_exists( 'Akismet', 'http_post' ) ) {
+ $response = Akismet::http_post( $query_string, 'comment-check' );
+ } else {
+ $response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
+ }
+
+ $result = false;
+
+ if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' ) {
+ $result = new WP_Error( 'feedback-discarded', __( 'Feedback discarded.', 'jetpack' ) );
+ } elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) { // 'true' is spam
+ $result = true;
+ }
+
+ /**
+ * Filter the results returned by Akismet for each submitted contact form.
+ *
+ * @module contact-form
+ *
+ * @since 1.3.1
+ *
+ * @param WP_Error|bool $result Is the submitted feedback spam.
+ * @param array|bool $form Submitted feedback.
+ */
+ return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
+ }
+
+ /**
+ * Submit a feedback as either spam or ham
+ *
+ * @param string $as Either 'spam' or 'ham'.
+ * @param array $form the contact-form data
+ */
+ function akismet_submit( $as, $form ) {
+ global $akismet_api_host, $akismet_api_port;
+
+ if ( ! in_array( $as, array( 'ham', 'spam' ) ) ) {
+ return false;
+ }
+
+ $query_string = '';
+ if ( is_array( $form ) ) {
+ $query_string = http_build_query( $form );
+ }
+ if ( method_exists( 'Akismet', 'http_post' ) ) {
+ $response = Akismet::http_post( $query_string, "submit-{$as}" );
+ } else {
+ $response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
+ }
+
+ return trim( $response[1] );
+ }
+
+ /**
+ * Prints the menu
+ */
+ function export_form() {
+ $current_screen = get_current_screen();
+ if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ) ) ) {
+ return;
+ }
+
+ if ( ! current_user_can( 'export' ) ) {
+ return;
+ }
+
+ // if there aren't any feedbacks, bail out
+ if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
+ return;
+ }
+ ?>
+
+ <div id="feedback-export" style="display:none">
+ <h2><?php _e( 'Export feedback as CSV', 'jetpack' ); ?></h2>
+ <div class="clear"></div>
+ <form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
+ <?php wp_nonce_field( 'feedback_export', 'feedback_export_nonce' ); ?>
+
+ <input name="action" value="feedback_export" type="hidden">
+ <label for="post"><?php _e( 'Select feedback to download', 'jetpack' ); ?></label>
+ <select name="post">
+ <option value="all"><?php esc_html_e( 'All posts', 'jetpack' ); ?></option>
+ <?php echo $this->get_feedbacks_as_options(); ?>
+ </select>
+
+ <br><br>
+ <input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
+ </form>
+ </div>
+
+ <?php
+ // There aren't any usable actions in core to output the "export feedback" form in the correct place,
+ // so this inline JS moves it from the top of the page to the bottom.
+ ?>
+ <script type='text/javascript'>
+ var menu = document.getElementById( 'feedback-export' ),
+ wrapper = document.getElementsByClassName( 'wrap' )[0];
+ <?php if ( 'edit-feedback' === $current_screen->id ) : ?>
+ wrapper.appendChild(menu);
+ <?php endif; ?>
+ menu.style.display = 'block';
+ </script>
+ <?php
+ }
+
+ /**
+ * Fetch post content for a post and extract just the comment.
+ *
+ * @param int $post_id The post id to fetch the content for.
+ *
+ * @return string Trimmed post comment.
+ *
+ * @codeCoverageIgnore
+ */
+ public function get_post_content_for_csv_export( $post_id ) {
+ $post_content = get_post_field( 'post_content', $post_id );
+ $content = explode( '<!--more-->', $post_content );
+
+ return trim( $content[0] );
+ }
+
+ /**
+ * Get `_feedback_extra_fields` field from post meta data.
+ *
+ * @param int $post_id Id of the post to fetch meta data for.
+ *
+ * @return mixed
+ *
+ * @codeCoverageIgnore - No need to be covered.
+ */
+ public function get_post_meta_for_csv_export( $post_id ) {
+ return get_post_meta( $post_id, '_feedback_extra_fields', true );
+ }
+
+ /**
+ * Get parsed feedback post fields.
+ *
+ * @param int $post_id Id of the post to fetch parsed contents for.
+ *
+ * @return array
+ *
+ * @codeCoverageIgnore - No need to be covered.
+ */
+ public function get_parsed_field_contents_of_post( $post_id ) {
+ return self::parse_fields_from_content( $post_id );
+ }
+
+ /**
+ * Properly maps fields that are missing from the post meta data
+ * to names, that are similar to those of the post meta.
+ *
+ * @param array $parsed_post_content Parsed post content
+ *
+ * @see parse_fields_from_content for how the input data is generated.
+ *
+ * @return array Mapped fields.
+ */
+ public function map_parsed_field_contents_of_post_to_field_names( $parsed_post_content ) {
+
+ $mapped_fields = array();
+
+ $field_mapping = array(
+ '_feedback_subject' => __( 'Contact Form', 'jetpack' ),
+ '_feedback_author' => '1_Name',
+ '_feedback_author_email' => '2_Email',
+ '_feedback_author_url' => '3_Website',
+ '_feedback_main_comment' => '4_Comment',
+ );
+
+ foreach ( $field_mapping as $parsed_field_name => $field_name ) {
+ if (
+ isset( $parsed_post_content[ $parsed_field_name ] )
+ && ! empty( $parsed_post_content[ $parsed_field_name ] )
+ ) {
+ $mapped_fields[ $field_name ] = $parsed_post_content[ $parsed_field_name ];
+ }
+ }
+
+ return $mapped_fields;
+ }
+
+ /**
+ * Registers the personal data exporter.
+ *
+ * @since 6.1.1
+ *
+ * @param array $exporters An array of personal data exporters.
+ *
+ * @return array $exporters An array of personal data exporters.
+ */
+ public function register_personal_data_exporter( $exporters ) {
+ $exporters['jetpack-feedback'] = array(
+ 'exporter_friendly_name' => __( 'Feedback', 'jetpack' ),
+ 'callback' => array( $this, 'personal_data_exporter' ),
+ );
+
+ return $exporters;
+ }
+
+ /**
+ * Registers the personal data eraser.
+ *
+ * @since 6.1.1
+ *
+ * @param array $erasers An array of personal data erasers.
+ *
+ * @return array $erasers An array of personal data erasers.
+ */
+ public function register_personal_data_eraser( $erasers ) {
+ $erasers['jetpack-feedback'] = array(
+ 'eraser_friendly_name' => __( 'Feedback', 'jetpack' ),
+ 'callback' => array( $this, 'personal_data_eraser' ),
+ );
+
+ return $erasers;
+ }
+
+ /**
+ * Exports personal data.
+ *
+ * @since 6.1.1
+ *
+ * @param string $email Email address.
+ * @param int $page Page to export.
+ *
+ * @return array $return Associative array with keys expected by core.
+ */
+ public function personal_data_exporter( $email, $page = 1 ) {
+ return $this->_internal_personal_data_exporter( $email, $page );
+ }
+
+ /**
+ * Internal method for exporting personal data.
+ *
+ * Allows us to have a different signature than core expects
+ * while protecting against future core API changes.
+ *
+ * @internal
+ * @since 6.5
+ *
+ * @param string $email Email address.
+ * @param int $page Page to export.
+ * @param int $per_page Number of feedbacks to process per page. Internal use only (testing)
+ *
+ * @return array Associative array with keys expected by core.
+ */
+ public function _internal_personal_data_exporter( $email, $page = 1, $per_page = 250 ) {
+ $export_data = array();
+ $post_ids = $this->personal_data_post_ids_by_email( $email, $per_page, $page );
+
+ foreach ( $post_ids as $post_id ) {
+ $post_fields = $this->get_parsed_field_contents_of_post( $post_id );
+
+ if ( ! is_array( $post_fields ) || empty( $post_fields['_feedback_subject'] ) ) {
+ continue; // Corrupt data.
+ }
+
+ $post_fields['_feedback_main_comment'] = $this->get_post_content_for_csv_export( $post_id );
+ $post_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_fields );
+
+ if ( ! is_array( $post_fields ) || empty( $post_fields ) ) {
+ continue; // No fields to export.
+ }
+
+ $post_meta = $this->get_post_meta_for_csv_export( $post_id );
+ $post_meta = is_array( $post_meta ) ? $post_meta : array();
+
+ $post_export_data = array();
+ $post_data = array_merge( $post_fields, $post_meta );
+ ksort( $post_data );
+
+ foreach ( $post_data as $post_data_key => $post_data_value ) {
+ $post_export_data[] = array(
+ 'name' => preg_replace( '/^[0-9]+_/', '', $post_data_key ),
+ 'value' => $post_data_value,
+ );
+ }
+
+ $export_data[] = array(
+ 'group_id' => 'feedback',
+ 'group_label' => __( 'Feedback', 'jetpack' ),
+ 'item_id' => 'feedback-' . $post_id,
+ 'data' => $post_export_data,
+ );
+ }
+
+ return array(
+ 'data' => $export_data,
+ 'done' => count( $post_ids ) < $per_page,
+ );
+ }
+
+ /**
+ * Erases personal data.
+ *
+ * @since 6.1.1
+ *
+ * @param string $email Email address.
+ * @param int $page Page to erase.
+ *
+ * @return array Associative array with keys expected by core.
+ */
+ public function personal_data_eraser( $email, $page = 1 ) {
+ return $this->_internal_personal_data_eraser( $email, $page );
+ }
+
+ /**
+ * Internal method for erasing personal data.
+ *
+ * Allows us to have a different signature than core expects
+ * while protecting against future core API changes.
+ *
+ * @internal
+ * @since 6.5
+ *
+ * @param string $email Email address.
+ * @param int $page Page to erase.
+ * @param int $per_page Number of feedbacks to process per page. Internal use only (testing)
+ *
+ * @return array Associative array with keys expected by core.
+ */
+ public function _internal_personal_data_eraser( $email, $page = 1, $per_page = 250 ) {
+ $removed = false;
+ $retained = false;
+ $messages = array();
+ $option_name = sprintf( '_jetpack_pde_feedback_%s', md5( $email ) );
+ $last_post_id = 1 === $page ? 0 : get_option( $option_name, 0 );
+ $post_ids = $this->personal_data_post_ids_by_email( $email, $per_page, $page, $last_post_id );
+
+ foreach ( $post_ids as $post_id ) {
+ /**
+ * Filters whether to erase a particular Feedback post.
+ *
+ * @since 6.3.0
+ *
+ * @param bool|string $prevention_message Whether to apply erase the Feedback post (bool).
+ * Custom prevention message (string). Default true.
+ * @param int $post_id Feedback post ID.
+ */
+ $prevention_message = apply_filters( 'grunion_contact_form_delete_feedback_post', true, $post_id );
+
+ if ( true !== $prevention_message ) {
+ if ( $prevention_message && is_string( $prevention_message ) ) {
+ $messages[] = esc_html( $prevention_message );
+ } else {
+ $messages[] = sprintf(
+ // translators: %d: Post ID.
+ __( 'Feedback ID %d could not be removed at this time.', 'jetpack' ),
+ $post_id
+ );
+ }
+
+ $retained = true;
+
+ continue;
+ }
+
+ if ( wp_delete_post( $post_id, true ) ) {
+ $removed = true;
+ } else {
+ $retained = true;
+ $messages[] = sprintf(
+ // translators: %d: Post ID.
+ __( 'Feedback ID %d could not be removed at this time.', 'jetpack' ),
+ $post_id
+ );
+ }
+ }
+
+ $done = count( $post_ids ) < $per_page;
+
+ if ( $done ) {
+ delete_option( $option_name );
+ } else {
+ update_option( $option_name, (int) $post_id );
+ }
+
+ return array(
+ 'items_removed' => $removed,
+ 'items_retained' => $retained,
+ 'messages' => $messages,
+ 'done' => $done,
+ );
+ }
+
+ /**
+ * Queries personal data by email address.
+ *
+ * @since 6.1.1
+ *
+ * @param string $email Email address.
+ * @param int $per_page Post IDs per page. Default is `250`.
+ * @param int $page Page to query. Default is `1`.
+ * @param int $last_post_id Page to query. Default is `0`. If non-zero, used instead of $page.
+ *
+ * @return array An array of post IDs.
+ */
+ public function personal_data_post_ids_by_email( $email, $per_page = 250, $page = 1, $last_post_id = 0 ) {
+ add_filter( 'posts_search', array( $this, 'personal_data_search_filter' ) );
+
+ $this->pde_last_post_id_erased = $last_post_id;
+ $this->pde_email_address = $email;
+
+ $post_ids = get_posts(
+ array(
+ 'post_type' => 'feedback',
+ 'post_status' => 'publish',
+ // This search parameter gets overwritten in ->personal_data_search_filter()
+ 's' => '..PDE..AUTHOR EMAIL:..PDE..',
+ 'sentence' => true,
+ 'order' => 'ASC',
+ 'orderby' => 'ID',
+ 'fields' => 'ids',
+ 'posts_per_page' => $per_page,
+ 'paged' => $last_post_id ? 1 : $page,
+ 'suppress_filters' => false,
+ )
+ );
+
+ $this->pde_last_post_id_erased = 0;
+ $this->pde_email_address = '';
+
+ remove_filter( 'posts_search', array( $this, 'personal_data_search_filter' ) );
+
+ return $post_ids;
+ }
+
+ /**
+ * Filters searches by email address.
+ *
+ * @since 6.1.1
+ *
+ * @param string $search SQL where clause.
+ *
+ * @return array Filtered SQL where clause.
+ */
+ public function personal_data_search_filter( $search ) {
+ global $wpdb;
+
+ /*
+ * Limits search to `post_content` only, and we only match the
+ * author's email address whenever it's on a line by itself.
+ */
+ if ( $this->pde_email_address && false !== strpos( $search, '..PDE..AUTHOR EMAIL:..PDE..' ) ) {
+ $search = $wpdb->prepare(
+ " AND (
+ {$wpdb->posts}.post_content LIKE %s
+ OR {$wpdb->posts}.post_content LIKE %s
+ )",
+ // `chr( 10 )` = `\n`, `chr( 13 )` = `\r`
+ '%' . $wpdb->esc_like( chr( 10 ) . 'AUTHOR EMAIL: ' . $this->pde_email_address . chr( 10 ) ) . '%',
+ '%' . $wpdb->esc_like( chr( 13 ) . 'AUTHOR EMAIL: ' . $this->pde_email_address . chr( 13 ) ) . '%'
+ );
+
+ if ( $this->pde_last_post_id_erased ) {
+ $search .= $wpdb->prepare( " AND {$wpdb->posts}.ID > %d", $this->pde_last_post_id_erased );
+ }
+ }
+
+ return $search;
+ }
+
+ /**
+ * Prepares feedback post data for CSV export.
+ *
+ * @param array $post_ids Post IDs to fetch the data for. These need to be Feedback posts.
+ *
+ * @return array
+ */
+ public function get_export_data_for_posts( $post_ids ) {
+
+ $posts_data = array();
+ $field_names = array();
+ $result = array();
+
+ /**
+ * Fetch posts and get the possible field names for later use
+ */
+ foreach ( $post_ids as $post_id ) {
+
+ /**
+ * Fetch post main data, because we need the subject and author data for the feedback form.
+ */
+ $post_real_data = $this->get_parsed_field_contents_of_post( $post_id );
+
+ /**
+ * If `$post_real_data` is not an array or there is no `_feedback_subject` set,
+ * then something must be wrong with the feedback post. Skip it.
+ */
+ if ( ! is_array( $post_real_data ) || ! isset( $post_real_data['_feedback_subject'] ) ) {
+ continue;
+ }
+
+ /**
+ * Fetch main post comment. This is from the default textarea fields.
+ * If it is non-empty, then we add it to data, otherwise skip it.
+ */
+ $post_comment_content = $this->get_post_content_for_csv_export( $post_id );
+ if ( ! empty( $post_comment_content ) ) {
+ $post_real_data['_feedback_main_comment'] = $post_comment_content;
+ }
+
+ /**
+ * Map parsed fields to proper field names
+ */
+ $mapped_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_real_data );
+
+ /**
+ * Fetch post meta data.
+ */
+ $post_meta_data = $this->get_post_meta_for_csv_export( $post_id );
+
+ /**
+ * If `$post_meta_data` is not an array or if it is empty, then there is no
+ * extra feedback to work with. Create an empty array.
+ */
+ if ( ! is_array( $post_meta_data ) || empty( $post_meta_data ) ) {
+ $post_meta_data = array();
+ }
+
+ /**
+ * Prepend the feedback subject to the list of fields.
+ */
+ $post_meta_data = array_merge(
+ $mapped_fields,
+ $post_meta_data
+ );
+
+ /**
+ * Save post metadata for later usage.
+ */
+ $posts_data[ $post_id ] = $post_meta_data;
+
+ /**
+ * Save field names, so we can use them as header fields later in the CSV.
+ */
+ $field_names = array_merge( $field_names, array_keys( $post_meta_data ) );
+ }
+
+ /**
+ * Make sure the field names are unique, because we don't want duplicate data.
+ */
+ $field_names = array_unique( $field_names );
+
+ /**
+ * Sort the field names by the field id number
+ */
+ sort( $field_names, SORT_NUMERIC );
+
+ /**
+ * Loop through every post, which is essentially CSV row.
+ */
+ foreach ( $posts_data as $post_id => $single_post_data ) {
+
+ /**
+ * Go through all the possible fields and check if the field is available
+ * in the current post.
+ *
+ * If it is - add the data as a value.
+ * If it is not - add an empty string, which is just a placeholder in the CSV.
+ */
+ foreach ( $field_names as $single_field_name ) {
+ if (
+ isset( $single_post_data[ $single_field_name ] )
+ && ! empty( $single_post_data[ $single_field_name ] )
+ ) {
+ $result[ $single_field_name ][] = trim( $single_post_data[ $single_field_name ] );
+ } else {
+ $result[ $single_field_name ][] = '';
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * download as a csv a contact form or all of them in a csv file
+ */
+ function download_feedback_as_csv() {
+ if ( empty( $_POST['feedback_export_nonce'] ) ) {
+ return;
+ }
+
+ check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
+
+ if ( ! current_user_can( 'export' ) ) {
+ return;
+ }
+
+ $args = array(
+ 'posts_per_page' => -1,
+ 'post_type' => 'feedback',
+ 'post_status' => 'publish',
+ 'order' => 'ASC',
+ 'fields' => 'ids',
+ 'suppress_filters' => false,
+ );
+
+ $filename = date( 'Y-m-d' ) . '-feedback-export.csv';
+
+ // Check if we want to download all the feedbacks or just a certain contact form
+ if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
+ $args['post_parent'] = (int) $_POST['post'];
+ $filename = date( 'Y-m-d' ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
+ }
+
+ $feedbacks = get_posts( $args );
+
+ if ( empty( $feedbacks ) ) {
+ return;
+ }
+
+ $filename = sanitize_file_name( $filename );
+
+ /**
+ * Prepare data for export.
+ */
+ $data = $this->get_export_data_for_posts( $feedbacks );
+
+ /**
+ * If `$data` is empty, there's nothing we can do below.
+ */
+ if ( ! is_array( $data ) || empty( $data ) ) {
+ return;
+ }
+
+ /**
+ * Extract field names from `$data` for later use.
+ */
+ $fields = array_keys( $data );
+
+ /**
+ * Count how many rows will be exported.
+ */
+ $row_count = count( reset( $data ) );
+
+ // Forces the download of the CSV instead of echoing
+ header( 'Content-Disposition: attachment; filename=' . $filename );
+ header( 'Pragma: no-cache' );
+ header( 'Expires: 0' );
+ header( 'Content-Type: text/csv; charset=utf-8' );
+
+ $output = fopen( 'php://output', 'w' );
+
+ /**
+ * Print CSV headers
+ */
+ fputcsv( $output, $fields );
+
+ /**
+ * Print rows to the output.
+ */
+ for ( $i = 0; $i < $row_count; $i ++ ) {
+
+ $current_row = array();
+
+ /**
+ * Put all the fields in `$current_row` array.
+ */
+ foreach ( $fields as $single_field_name ) {
+ $current_row[] = $this->esc_csv( $data[ $single_field_name ][ $i ] );
+ }
+
+ /**
+ * Output the complete CSV row
+ */
+ fputcsv( $output, $current_row );
+ }
+
+ fclose( $output );
+ }
+
+ /**
+ * Escape a string to be used in a CSV context
+ *
+ * Malicious input can inject formulas into CSV files, opening up the possibility for phishing attacks and
+ * disclosure of sensitive information.
+ *
+ * Additionally, Excel exposes the ability to launch arbitrary commands through the DDE protocol.
+ *
+ * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
+ *
+ * @param string $field
+ *
+ * @return string
+ */
+ public function esc_csv( $field ) {
+ $active_content_triggers = array( '=', '+', '-', '@' );
+
+ if ( in_array( mb_substr( $field, 0, 1 ), $active_content_triggers, true ) ) {
+ $field = "'" . $field;
+ }
+
+ return $field;
+ }
+
+ /**
+ * Returns a string of HTML <option> items from an array of posts
+ *
+ * @return string a string of HTML <option> items
+ */
+ protected function get_feedbacks_as_options() {
+ $options = '';
+
+ // Get the feedbacks' parents' post IDs
+ $feedbacks = get_posts(
+ array(
+ 'fields' => 'id=>parent',
+ 'posts_per_page' => 100000,
+ 'post_type' => 'feedback',
+ 'post_status' => 'publish',
+ 'suppress_filters' => false,
+ )
+ );
+ $parents = array_unique( array_values( $feedbacks ) );
+
+ $posts = get_posts(
+ array(
+ 'orderby' => 'ID',
+ 'posts_per_page' => 1000,
+ 'post_type' => 'any',
+ 'post__in' => array_values( $parents ),
+ 'suppress_filters' => false,
+ )
+ );
+
+ // creates the string of <option> elements
+ foreach ( $posts as $post ) {
+ $options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
+ }
+
+ return $options;
+ }
+
+ /**
+ * Get the names of all the form's fields
+ *
+ * @param array|int $posts the post we want the fields of
+ *
+ * @return array the array of fields
+ *
+ * @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
+ */
+ protected function get_field_names( $posts ) {
+ $posts = (array) $posts;
+ $all_fields = array();
+
+ foreach ( $posts as $post ) {
+ $fields = self::parse_fields_from_content( $post );
+
+ if ( isset( $fields['_feedback_all_fields'] ) ) {
+ $extra_fields = array_keys( $fields['_feedback_all_fields'] );
+ $all_fields = array_merge( $all_fields, $extra_fields );
+ }
+ }
+
+ $all_fields = array_unique( $all_fields );
+ return $all_fields;
+ }
+
+ public static function parse_fields_from_content( $post_id ) {
+ static $post_fields;
+
+ if ( ! is_array( $post_fields ) ) {
+ $post_fields = array();
+ }
+
+ if ( isset( $post_fields[ $post_id ] ) ) {
+ return $post_fields[ $post_id ];
+ }
+
+ $all_values = array();
+ $post_content = get_post_field( 'post_content', $post_id );
+ $content = explode( '<!--more-->', $post_content );
+ $lines = array();
+
+ if ( count( $content ) > 1 ) {
+ $content = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
+ $one_line = preg_replace( '/\s+/', ' ', $content );
+ $one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
+
+ preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
+
+ if ( count( $matches ) > 1 ) {
+ $all_values = array_combine( array_map( 'trim', $matches[1] ), array_map( 'trim', $matches[2] ) );
+ }
+
+ $lines = array_filter( explode( "\n", $content ) );
+ }
+
+ $var_map = array(
+ 'AUTHOR' => '_feedback_author',
+ 'AUTHOR EMAIL' => '_feedback_author_email',
+ 'AUTHOR URL' => '_feedback_author_url',
+ 'SUBJECT' => '_feedback_subject',
+ 'IP' => '_feedback_ip',
+ );
+
+ $fields = array();
+
+ foreach ( $lines as $line ) {
+ $vars = explode( ': ', $line, 2 );
+ if ( ! empty( $vars ) ) {
+ if ( isset( $var_map[ $vars[0] ] ) ) {
+ $fields[ $var_map[ $vars[0] ] ] = self::strip_tags( trim( $vars[1] ) );
+ }
+ }
+ }
+
+ $fields['_feedback_all_fields'] = $all_values;
+
+ $post_fields[ $post_id ] = $fields;
+
+ return $fields;
+ }
+
+ /**
+ * Creates a valid csv row from a post id
+ *
+ * @param int $post_id The id of the post
+ * @param array $fields An array containing the names of all the fields of the csv
+ * @return String The csv row
+ *
+ * @deprecated This is no longer needed, as of the CSV export rewrite.
+ */
+ protected static function make_csv_row_from_feedback( $post_id, $fields ) {
+ $content_fields = self::parse_fields_from_content( $post_id );
+ $all_fields = array();
+
+ if ( isset( $content_fields['_feedback_all_fields'] ) ) {
+ $all_fields = $content_fields['_feedback_all_fields'];
+ }
+
+ // Overwrite the parsed content with the content we stored in post_meta in a better format.
+ $extra_fields = get_post_meta( $post_id, '_feedback_extra_fields', true );
+ foreach ( $extra_fields as $extra_field => $extra_value ) {
+ $all_fields[ $extra_field ] = $extra_value;
+ }
+
+ // The first element in all of the exports will be the subject
+ $row_items[] = $content_fields['_feedback_subject'];
+
+ // Loop the fields array in order to fill the $row_items array correctly
+ foreach ( $fields as $field ) {
+ if ( $field === __( 'Contact Form', 'jetpack' ) ) { // the first field will ever be the contact form, so we can continue
+ continue;
+ } elseif ( array_key_exists( $field, $all_fields ) ) {
+ $row_items[] = $all_fields[ $field ];
+ } else {
+ $row_items[] = '';
+ }
+ }
+
+ return $row_items;
+ }
+
+ public static function get_ip_address() {
+ return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
+ }
+}
+
+/**
+ * Generic shortcode class.
+ * Does nothing other than store structured data and output the shortcode as a string
+ *
+ * Not very general - specific to Grunion.
+ */
+class Crunion_Contact_Form_Shortcode {
+ /**
+ * @var string the name of the shortcode: [$shortcode_name /]
+ */
+ public $shortcode_name;
+
+ /**
+ * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
+ */
+ public $attributes;
+
+ /**
+ * @var array key => value pair for attribute defaults
+ */
+ public $defaults = array();
+
+ /**
+ * @var null|string Null for selfclosing shortcodes. Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
+ */
+ public $content;
+
+ /**
+ * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
+ */
+ public $fields;
+
+ /**
+ * @var null|string The HTML of the parsed inner "child" shortcodes". Null for selfclosing shortcodes.
+ */
+ public $body;
+
+ /**
+ * @param array $attributes An associative array of shortcode attributes. @see shortcode_atts()
+ * @param null|string $content Null for selfclosing shortcodes. The inner content otherwise.
+ */
+ function __construct( $attributes, $content = null ) {
+ $this->attributes = $this->unesc_attr( $attributes );
+ if ( is_array( $content ) ) {
+ $string_content = '';
+ foreach ( $content as $field ) {
+ $string_content .= (string) $field;
+ }
+
+ $this->content = $string_content;
+ } else {
+ $this->content = $content;
+ }
+
+ $this->parse_content( $this->content );
+ }
+
+ /**
+ * Processes the shortcode's inner content for "child" shortcodes
+ *
+ * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
+ */
+ function parse_content( $content ) {
+ if ( is_null( $content ) ) {
+ $this->body = null;
+ }
+
+ $this->body = do_shortcode( $content );
+ }
+
+ /**
+ * Returns the value of the requested attribute.
+ *
+ * @param string $key The attribute to retrieve
+ * @return mixed
+ */
+ function get_attribute( $key ) {
+ return isset( $this->attributes[ $key ] ) ? $this->attributes[ $key ] : null;
+ }
+
+ function esc_attr( $value ) {
+ if ( is_array( $value ) ) {
+ return array_map( array( $this, 'esc_attr' ), $value );
+ }
+
+ $value = Grunion_Contact_Form_Plugin::strip_tags( $value );
+ $value = _wp_specialchars( $value, ENT_QUOTES, false, true );
+
+ // Shortcode attributes can't contain "]"
+ $value = str_replace( ']', '', $value );
+ $value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
+ $value = strtr(
+ $value, array(
+ '%' => '%25',
+ '&' => '%26',
+ )
+ );
+
+ // shortcode_parse_atts() does stripcslashes()
+ $value = addslashes( $value );
+ return $value;
+ }
+
+ function unesc_attr( $value ) {
+ if ( is_array( $value ) ) {
+ return array_map( array( $this, 'unesc_attr' ), $value );
+ }
+
+ // For back-compat with old Grunion encoding
+ // Also, unencode commas
+ $value = strtr(
+ $value, array(
+ '%26' => '&',
+ '%25' => '%',
+ )
+ );
+ $value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
+ $value = htmlspecialchars_decode( $value, ENT_QUOTES );
+ $value = Grunion_Contact_Form_Plugin::strip_tags( $value );
+
+ return $value;
+ }
+
+ /**
+ * Generates the shortcode
+ */
+ function __toString() {
+ $r = "[{$this->shortcode_name} ";
+
+ foreach ( $this->attributes as $key => $value ) {
+ if ( ! $value ) {
+ continue;
+ }
+
+ if ( isset( $this->defaults[ $key ] ) && $this->defaults[ $key ] == $value ) {
+ continue;
+ }
+
+ if ( 'id' == $key ) {
+ continue;
+ }
+
+ $value = $this->esc_attr( $value );
+
+ if ( is_array( $value ) ) {
+ $value = join( ',', $value );
+ }
+
+ if ( false === strpos( $value, "'" ) ) {
+ $value = "'$value'";
+ } elseif ( false === strpos( $value, '"' ) ) {
+ $value = '"' . $value . '"';
+ } else {
+ // Shortcodes can't contain both '"' and "'". Strip one.
+ $value = str_replace( "'", '', $value );
+ $value = "'$value'";
+ }
+
+ $r .= "{$key}={$value} ";
+ }
+
+ $r = rtrim( $r );
+
+ if ( $this->fields ) {
+ $r .= ']';
+
+ foreach ( $this->fields as $field ) {
+ $r .= (string) $field;
+ }
+
+ $r .= "[/{$this->shortcode_name}]";
+ } else {
+ $r .= '/]';
+ }
+
+ return $r;
+ }
+}
+
+/**
+ * Class for the contact-form shortcode.
+ * Parses shortcode to output the contact form as HTML
+ * Sends email and stores the contact form response (a.k.a. "feedback")
+ */
+class Grunion_Contact_Form extends Crunion_Contact_Form_Shortcode {
+ public $shortcode_name = 'contact-form';
+
+ /**
+ * @var WP_Error stores form submission errors
+ */
+ public $errors;
+
+ /**
+ * @var string The SHA1 hash of the attributes that comprise the form.
+ */
+ public $hash;
+
+ /**
+ * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
+ */
+ static $last;
+
+ /**
+ * @var Whatever form we are currently looking at. If processed, will become $last
+ */
+ static $current_form;
+
+ /**
+ * @var array All found forms, indexed by hash.
+ */
+ static $forms = array();
+
+ /**
+ * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
+ */
+ static $style = false;
+
+ /**
+ * @var array When printing the submit button, what tags are allowed
+ */
+ static $allowed_html_tags_for_submit_button = array( 'br' => array() );
+
+ function __construct( $attributes, $content = null ) {
+ global $post;
+
+ $this->hash = sha1( json_encode( $attributes ) . $content );
+ self::$forms[ $this->hash ] = $this;
+
+ // Set up the default subject and recipient for this form
+ $default_to = '';
+ $default_subject = '[' . get_option( 'blogname' ) . ']';
+
+ if ( ! isset( $attributes ) || ! is_array( $attributes ) ) {
+ $attributes = array();
+ }
+
+ if ( ! empty( $attributes['widget'] ) && $attributes['widget'] ) {
+ $default_to .= get_option( 'admin_email' );
+ $attributes['id'] = 'widget-' . $attributes['widget'];
+ $default_subject = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
+ } elseif ( $post ) {
+ $attributes['id'] = $post->ID;
+ $default_subject = sprintf( _x( '%1$s %2$s', '%1$s = blog name, %2$s = post title', 'jetpack' ), $default_subject, Grunion_Contact_Form_Plugin::strip_tags( $post->post_title ) );
+ $post_author = get_userdata( $post->post_author );
+ $default_to .= $post_author->user_email;
+ }
+
+ // Keep reference to $this for parsing form fields
+ self::$current_form = $this;
+
+ $this->defaults = array(
+ 'to' => $default_to,
+ 'subject' => $default_subject,
+ 'show_subject' => 'no', // only used in back-compat mode
+ 'widget' => 0, // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
+ 'id' => null, // Not exposed to the user. Set above.
+ 'submit_button_text' => __( 'Submit', 'jetpack' ),
+ );
+
+ $attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
+
+ // We only enable the contact-field shortcode temporarily while processing the contact-form shortcode
+ Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
+
+ parent::__construct( $attributes, $content );
+
+ // There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
+ if ( empty( $this->fields ) ) {
+ // same as the original Grunion v1 form
+ $default_form = '
+ [contact-field label="' . __( 'Name', 'jetpack' ) . '" type="name" required="true" /]
+ [contact-field label="' . __( 'Email', 'jetpack' ) . '" type="email" required="true" /]
+ [contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
+
+ if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
+ $default_form .= '
+ [contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
+ }
+
+ $default_form .= '
+ [contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
+
+ $this->parse_content( $default_form );
+
+ // Store the shortcode
+ $this->store_shortcode( $default_form, $attributes, $this->hash );
+ } else {
+ // Store the shortcode
+ $this->store_shortcode( $content, $attributes, $this->hash );
+ }
+
+ // $this->body and $this->fields have been setup. We no longer need the contact-field shortcode.
+ Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
+ }
+
+ /**
+ * Store shortcode content for recall later
+ * - used to receate shortcode when user uses do_shortcode
+ *
+ * @param string $content
+ * @param array $attributes
+ * @param string $hash
+ */
+ static function store_shortcode( $content = null, $attributes = null, $hash = null ) {
+
+ if ( $content != null and isset( $attributes['id'] ) ) {
+
+ if ( empty( $hash ) ) {
+ $hash = sha1( json_encode( $attributes ) . $content );
+ }
+
+ $shortcode_meta = get_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", true );
+
+ if ( $shortcode_meta != '' or $shortcode_meta != $content ) {
+ update_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", $content );
+
+ // Save attributes to post_meta for later use. They're not available later in do_shortcode situations.
+ update_post_meta( $attributes['id'], "_g_feedback_shortcode_atts_{$hash}", $attributes );
+ }
+ }
+ }
+
+ /**
+ * Toggle for printing the grunion.css stylesheet
+ *
+ * @param bool $style
+ */
+ static function style( $style ) {
+ $previous_style = self::$style;
+ self::$style = (bool) $style;
+ return $previous_style;
+ }
+
+ /**
+ * Turn on printing of grunion.css stylesheet
+ *
+ * @see ::style()
+ * @internal
+ * @param bool $style
+ */
+ static function _style_on() {
+ return self::style( true );
+ }
+
+ /**
+ * The contact-form shortcode processor
+ *
+ * @param array $attributes Key => Value pairs as parsed by shortcode_parse_atts()
+ * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
+ * @return string HTML for the concat form.
+ */
+ static function parse( $attributes, $content ) {
+ require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
+ if ( Jetpack_Sync_Settings::is_syncing() ) {
+ return '';
+ }
+ // Create a new Grunion_Contact_Form object (this class)
+ $form = new Grunion_Contact_Form( $attributes, $content );
+
+ $id = $form->get_attribute( 'id' );
+
+ if ( ! $id ) { // something terrible has happened
+ return '[contact-form]';
+ }
+
+ if ( is_feed() ) {
+ return '[contact-form]';
+ }
+
+ self::$last = $form;
+
+ // Enqueue the grunion.css stylesheet if self::$style allows it
+ if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
+ // Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
+ // (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
+ // when WordPress does the real loop.
+ wp_enqueue_style( 'grunion.css' );
+ }
+
+ $r = '';
+ $r .= "<div id='contact-form-$id'>\n";
+
+ if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
+ // There are errors. Display them
+ $r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
+ foreach ( $form->errors->get_error_messages() as $message ) {
+ $r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
+ }
+ $r .= "</ul>\n</div>\n\n";
+ }
+
+ if ( isset( $_GET['contact-form-id'] )
+ && $_GET['contact-form-id'] == self::$last->get_attribute( 'id' )
+ && isset( $_GET['contact-form-sent'], $_GET['contact-form-hash'] )
+ && hash_equals( $form->hash, $_GET['contact-form-hash'] ) ) {
+ // The contact form was submitted. Show the success message/results
+ $feedback_id = (int) $_GET['contact-form-sent'];
+
+ $back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
+
+ $r_success_message =
+ '<h3>' . __( 'Message Sent', 'jetpack' ) .
+ ' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
+ "</h3>\n\n";
+
+ // Don't show the feedback details unless the nonce matches
+ if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
+ $r_success_message .= self::success_message( $feedback_id, $form );
+ }
+
+ /**
+ * Filter the message returned after a successful contact form submission.
+ *
+ * @module contact-form
+ *
+ * @since 1.3.1
+ *
+ * @param string $r_success_message Success message.
+ */
+ $r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
+ } else {
+ // Nothing special - show the normal contact form
+ if ( $form->get_attribute( 'widget' ) ) {
+ // Submit form to the current URL
+ $url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
+ } else {
+ // Submit form to the post permalink
+ $url = get_permalink();
+ }
+
+ // For SSL/TLS page. See RFC 3986 Section 4.2
+ $url = set_url_scheme( $url );
+
+ // May eventually want to send this to admin-post.php...
+ /**
+ * Filter the contact form action URL.
+ *
+ * @module contact-form
+ *
+ * @since 1.3.1
+ *
+ * @param string $contact_form_id Contact form post URL.
+ * @param $post $GLOBALS['post'] Post global variable.
+ * @param int $id Contact Form ID.
+ */
+ $url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
+
+ $r .= "<form action='" . esc_url( $url ) . "' method='post' class='contact-form commentsblock'>\n";
+ $r .= $form->body;
+ $r .= "\t<p class='contact-submit'>\n";
+
+ $gutenberg_submit_button_classes = '';
+ if ( ! empty( $attributes['submitButtonClasses'] ) ) {
+ $gutenberg_submit_button_classes = ' ' . $attributes['submitButtonClasses'];
+ }
+
+ /**
+ * Filter the contact form submit button class attribute.
+ *
+ * @module contact-form
+ *
+ * @since 6.6.0
+ *
+ * @param string $class Additional CSS classes for button attribute.
+ */
+ $submit_button_class = apply_filters( 'jetpack_contact_form_submit_button_class', 'pushbutton-wide' . $gutenberg_submit_button_classes );
+
+ $submit_button_styles = '';
+ if ( ! empty( $attributes['customBackgroundButtonColor'] ) ) {
+ $submit_button_styles .= 'background-color: ' . $attributes['customBackgroundButtonColor'] . '; ';
+ }
+ if ( ! empty( $attributes['customTextButtonColor'] ) ) {
+ $submit_button_styles .= 'color: ' . $attributes['customTextButtonColor'] . ';';
+ }
+ if ( ! empty( $attributes['submitButtonText'] ) ) {
+ $submit_button_text = $attributes['submitButtonText'];
+ } else {
+ $submit_button_text = $form->get_attribute( 'submit_button_text' );
+ }
+
+ $r .= "\t\t<button type='submit' class='" . esc_attr( $submit_button_class ) . "'";
+ if ( ! empty( $submit_button_styles ) ) {
+ $r .= " style='" . esc_attr( $submit_button_styles ) . "'";
+ }
+ $r .= ">";
+ $r .= wp_kses(
+ $submit_button_text,
+ self::$allowed_html_tags_for_submit_button
+ ) . "</button>";
+
+ if ( is_user_logged_in() ) {
+ $r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
+ }
+ $r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
+ $r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
+ $r .= "\t\t<input type='hidden' name='contact-form-hash' value='" . esc_attr( $form->hash ) . "' />\n";
+ $r .= "\t</p>\n";
+ $r .= "</form>\n";
+ }
+
+ $r .= '</div>';
+
+ return $r;
+ }
+
+ /**
+ * Returns a success message to be returned if the form is sent via AJAX.
+ *
+ * @param int $feedback_id
+ * @param object Grunion_Contact_Form $form
+ *
+ * @return string $message
+ */
+ static function success_message( $feedback_id, $form ) {
+ return wp_kses(
+ '<blockquote class="contact-form-submission">'
+ . '<p>' . join( self::get_compiled_form( $feedback_id, $form ), '</p><p>' ) . '</p>'
+ . '</blockquote>',
+ array(
+ 'br' => array(),
+ 'blockquote' => array( 'class' => array() ),
+ 'p' => array(),
+ )
+ );
+ }
+
+ /**
+ * Returns a compiled form with labels and values in a form of an array
+ * of lines.
+ *
+ * @param int $feedback_id
+ * @param object Grunion_Contact_Form $form
+ *
+ * @return array $lines
+ */
+ static function get_compiled_form( $feedback_id, $form ) {
+ $feedback = get_post( $feedback_id );
+ $field_ids = $form->get_field_ids();
+ $content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
+
+ // Maps field_ids to post_meta keys
+ $field_value_map = array(
+ 'name' => 'author',
+ 'email' => 'author_email',
+ 'url' => 'author_url',
+ 'subject' => 'subject',
+ 'textarea' => false, // not a post_meta key. This is stored in post_content
+ );
+
+ $compiled_form = array();
+
+ // "Standard" field whitelist
+ foreach ( $field_value_map as $type => $meta_key ) {
+ if ( isset( $field_ids[ $type ] ) ) {
+ $field = $form->fields[ $field_ids[ $type ] ];
+
+ if ( $meta_key ) {
+ if ( isset( $content_fields[ "_feedback_{$meta_key}" ] ) ) {
+ $value = $content_fields[ "_feedback_{$meta_key}" ];
+ }
+ } else {
+ // The feedback content is stored as the first "half" of post_content
+ $value = $feedback->post_content;
+ list( $value ) = explode( '<!--more-->', $value );
+ $value = trim( $value );
+ }
+
+ $field_index = array_search( $field_ids[ $type ], $field_ids['all'] );
+ $compiled_form[ $field_index ] = sprintf(
+ '<b>%1$s:</b> %2$s<br /><br />',
+ wp_kses( $field->get_attribute( 'label' ), array() ),
+ self::escape_and_sanitize_field_value( $value )
+ );
+ }
+ }
+
+ // "Non-standard" fields
+ if ( $field_ids['extra'] ) {
+ // array indexed by field label (not field id)
+ $extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
+
+ /**
+ * Only get data for the compiled form if `$extra_fields` is a valid and non-empty array.
+ */
+ if ( is_array( $extra_fields ) && ! empty( $extra_fields ) ) {
+
+ $extra_field_keys = array_keys( $extra_fields );
+
+ $i = 0;
+ foreach ( $field_ids['extra'] as $field_id ) {
+ $field = $form->fields[ $field_id ];
+ $field_index = array_search( $field_id, $field_ids['all'] );
+
+ $label = $field->get_attribute( 'label' );
+
+ $compiled_form[ $field_index ] = sprintf(
+ '<b>%1$s:</b> %2$s<br /><br />',
+ wp_kses( $label, array() ),
+ self::escape_and_sanitize_field_value( $extra_fields[ $extra_field_keys[ $i ] ] )
+ );
+
+ $i++;
+ }
+ }
+ }
+
+ // Sorting lines by the field index
+ ksort( $compiled_form );
+
+ return $compiled_form;
+ }
+
+ static function escape_and_sanitize_field_value( $value ) {
+ $value = str_replace( array( '[' , ']' ) , array( '&#91;' , '&#93;' ) , $value );
+ return nl2br( wp_kses( $value, array() ) );
+ }
+
+ /**
+ * Only strip out empty string values and keep all the other values as they are.
+ *
+ * @param $single_value
+ *
+ * @return bool
+ */
+ static function remove_empty( $single_value ) {
+ return ( $single_value !== '' );
+ }
+
+ /**
+ * The contact-field shortcode processor
+ * We use an object method here instead of a static Grunion_Contact_Form_Field class method to parse contact-field shortcodes so that we can tie them to the contact-form object.
+ *
+ * @param array $attributes Key => Value pairs as parsed by shortcode_parse_atts()
+ * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
+ * @return HTML for the contact form field
+ */
+ static function parse_contact_field( $attributes, $content ) {
+ // Don't try to parse contact form fields if not inside a contact form
+ if ( ! Grunion_Contact_Form_Plugin::$using_contact_form_field ) {
+ $att_strs = array();
+ if ( ! isset( $attributes['label'] ) ) {
+ $type = isset( $attributes['type'] ) ? $attributes['type'] : null;
+ $attributes['label'] = self::get_default_label_from_type( $type );
+ }
+ foreach ( $attributes as $att => $val ) {
+ if ( is_numeric( $att ) ) { // Is a valueless attribute
+ $att_strs[] = esc_html( $val );
+ } elseif ( isset( $val ) ) { // A regular attr - value pair
+ if ( ( $att === 'options' || $att === 'values' ) && is_string( $val ) ) { // remove any empty strings
+ $val = explode( ',', $val );
+ }
+ if ( is_array( $val ) ) {
+ $val = array_filter( $val, array( __CLASS__, 'remove_empty' ) ); // removes any empty strings
+ $att_strs[] = esc_html( $att ) . '="' . implode( ',', array_map( 'esc_html', $val ) ) . '"';
+ } elseif ( is_bool( $val ) ) {
+ $att_strs[] = esc_html( $att ) . '="' . esc_html( $val ? '1' : '' ) . '"';
+ } else {
+ $att_strs[] = esc_html( $att ) . '="' . esc_html( $val ) . '"';
+ }
+ }
+ }
+
+ $html = '[contact-field ' . implode( ' ', $att_strs );
+
+ if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
+ $html .= ']' . esc_html( $content ) . '[/contact-field]';
+ } else { // Otherwise let's add a closing slash in the first tag
+ $html .= '/]';
+ }
+
+ return $html;
+ }
+
+ $form = Grunion_Contact_Form::$current_form;
+
+ $field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
+
+ $field_id = $field->get_attribute( 'id' );
+ if ( $field_id ) {
+ $form->fields[ $field_id ] = $field;
+ } else {
+ $form->fields[] = $field;
+ }
+
+ if (
+ isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
+ &&
+ isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
+ &&
+ isset( $_POST['contact-form-hash'] ) && hash_equals( $form->hash, $_POST['contact-form-hash'] )
+ ) {
+ // If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
+ $field->validate();
+ }
+
+ // Output HTML
+ return $field->render();
+ }
+
+ static function get_default_label_from_type( $type ) {
+ switch ( $type ) {
+ case 'text':
+ return __( 'Text', 'jetpack' );
+ case 'name':
+ return __( 'Name', 'jetpack' );
+ case 'email':
+ return __( 'Email', 'jetpack' );
+ case 'url':
+ return __( 'Website', 'jetpack' );
+ case 'date':
+ return __( 'Date', 'jetpack' );
+ case 'telephone':
+ return __( 'Phone', 'jetpack' );
+ case 'textarea':
+ return __( 'Message', 'jetpack' );
+ case 'checkbox':
+ return __( 'Checkbox', 'jetpack' );
+ case 'checkbox-multiple':
+ return __( 'Choose several', 'jetpack' );
+ case 'radio':
+ return __( 'Choose one', 'jetpack' );
+ case 'select':
+ return __( 'Select one', 'jetpack' );
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Loops through $this->fields to generate a (structured) list of field IDs.
+ *
+ * Important: Currently the whitelisted fields are defined as follows:
+ * `name`, `email`, `url`, `subject`, `textarea`
+ *
+ * If you need to add new fields to the Contact Form, please don't add them
+ * to the whitelisted fields and leave them as extra fields.
+ *
+ * The reasoning behind this is that both the admin Feedback view and the CSV
+ * export will not include any fields that are added to the list of
+ * whitelisted fields without taking proper care to add them to all the
+ * other places where they accessed/used/saved.
+ *
+ * The safest way to add new fields is to add them to the dropdown and the
+ * HTML list ( @see Grunion_Contact_Form_Field::render ) and don't add them
+ * to the list of whitelisted fields. This way they will become a part of the
+ * `extra fields` which are saved in the post meta and will be properly
+ * handled by the admin Feedback view and the CSV Export without any extra
+ * work.
+ *
+ * If there is need to add a field to the whitelisted fields, then please
+ * take proper care to add logic to handle the field in the following places:
+ *
+ * - Below in the switch statement - so the field is recognized as whitelisted.
+ *
+ * - Grunion_Contact_Form::process_submission - validation and logic.
+ *
+ * - Grunion_Contact_Form::process_submission - add the field as an additional
+ * field in the `post_content` when saving the feedback content.
+ *
+ * - Grunion_Contact_Form_Plugin::parse_fields_from_content - add mapping
+ * for the field, defined in the above method.
+ *
+ * - Grunion_Contact_Form_Plugin::map_parsed_field_contents_of_post_to_field_names -
+ * add mapping of the field for the CSV Export. Otherwise it will be missing
+ * from the exported data.
+ *
+ * - admin.php / grunion_manage_post_columns - add the field to the render logic.
+ * Otherwise it will be missing from the admin Feedback view.
+ *
+ * @return array
+ */
+ function get_field_ids() {
+ $field_ids = array(
+ 'all' => array(), // array of all field_ids
+ 'extra' => array(), // array of all non-whitelisted field IDs
+
+ // Whitelisted "standard" field IDs:
+ // 'email' => field_id,
+ // 'name' => field_id,
+ // 'url' => field_id,
+ // 'subject' => field_id,
+ // 'textarea' => field_id,
+ );
+
+ foreach ( $this->fields as $id => $field ) {
+ $field_ids['all'][] = $id;
+
+ $type = $field->get_attribute( 'type' );
+ if ( isset( $field_ids[ $type ] ) ) {
+ // This type of field is already present in our whitelist of "standard" fields for this form
+ // Put it in extra
+ $field_ids['extra'][] = $id;
+ continue;
+ }
+
+ /**
+ * See method description before modifying the switch cases.
+ */
+ switch ( $type ) {
+ case 'email':
+ case 'name':
+ case 'url':
+ case 'subject':
+ case 'textarea':
+ $field_ids[ $type ] = $id;
+ break;
+ default:
+ // Put everything else in extra
+ $field_ids['extra'][] = $id;
+ }
+ }
+
+ return $field_ids;
+ }
+
+ /**
+ * Process the contact form's POST submission
+ * Stores feedback. Sends email.
+ */
+ function process_submission() {
+ global $post;
+
+ $plugin = Grunion_Contact_Form_Plugin::init();
+
+ $id = $this->get_attribute( 'id' );
+ $to = $this->get_attribute( 'to' );
+ $widget = $this->get_attribute( 'widget' );
+
+ $contact_form_subject = $this->get_attribute( 'subject' );
+
+ $to = str_replace( ' ', '', $to );
+ $emails = explode( ',', $to );
+
+ $valid_emails = array();
+
+ foreach ( (array) $emails as $email ) {
+ if ( ! is_email( $email ) ) {
+ continue;
+ }
+
+ if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
+ continue;
+ }
+
+ $valid_emails[] = $email;
+ }
+
+ // No one to send it to, which means none of the "to" attributes are valid emails.
+ // Use default email instead.
+ if ( ! $valid_emails ) {
+ $valid_emails = $this->defaults['to'];
+ }
+
+ $to = $valid_emails;
+
+ // Last ditch effort to set a recipient if somehow none have been set.
+ if ( empty( $to ) ) {
+ $to = get_option( 'admin_email' );
+ }
+
+ // Make sure we're processing the form we think we're processing... probably a redundant check.
+ if ( $widget ) {
+ if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
+ return false;
+ }
+ } else {
+ if ( $post->ID != $_POST['contact-form-id'] ) {
+ return false;
+ }
+ }
+
+ $field_ids = $this->get_field_ids();
+
+ // Initialize all these "standard" fields to null
+ $comment_author_email = $comment_author_email_label = // v
+ $comment_author = $comment_author_label = // v
+ $comment_author_url = $comment_author_url_label = // v
+ $comment_content = $comment_content_label = null;
+
+ // For each of the "standard" fields, grab their field label and value.
+ if ( isset( $field_ids['name'] ) ) {
+ $field = $this->fields[ $field_ids['name'] ];
+ $comment_author = Grunion_Contact_Form_Plugin::strip_tags(
+ stripslashes(
+ /** This filter is already documented in core/wp-includes/comment-functions.php */
+ apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
+ )
+ );
+ $comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
+ }
+
+ if ( isset( $field_ids['email'] ) ) {
+ $field = $this->fields[ $field_ids['email'] ];
+ $comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
+ stripslashes(
+ /** This filter is already documented in core/wp-includes/comment-functions.php */
+ apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
+ )
+ );
+ $comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
+ }
+
+ if ( isset( $field_ids['url'] ) ) {
+ $field = $this->fields[ $field_ids['url'] ];
+ $comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
+ stripslashes(
+ /** This filter is already documented in core/wp-includes/comment-functions.php */
+ apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
+ )
+ );
+ if ( 'http://' == $comment_author_url ) {
+ $comment_author_url = '';
+ }
+ $comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
+ }
+
+ if ( isset( $field_ids['textarea'] ) ) {
+ $field = $this->fields[ $field_ids['textarea'] ];
+ $comment_content = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
+ $comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
+ }
+
+ if ( isset( $field_ids['subject'] ) ) {
+ $field = $this->fields[ $field_ids['subject'] ];
+ if ( $field->value ) {
+ $contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
+ }
+ }
+
+ $all_values = $extra_values = array();
+ $i = 1; // Prefix counter for stored metadata
+
+ // For all fields, grab label and value
+ foreach ( $field_ids['all'] as $field_id ) {
+ $field = $this->fields[ $field_id ];
+ $label = $i . '_' . $field->get_attribute( 'label' );
+ $value = $field->value;
+
+ $all_values[ $label ] = $value;
+ $i++; // Increment prefix counter for the next field
+ }
+
+ // For the "non-standard" fields, grab label and value
+ // Extra fields have their prefix starting from count( $all_values ) + 1
+ foreach ( $field_ids['extra'] as $field_id ) {
+ $field = $this->fields[ $field_id ];
+ $label = $i . '_' . $field->get_attribute( 'label' );
+ $value = $field->value;
+
+ if ( is_array( $value ) ) {
+ $value = implode( ', ', $value );
+ }
+
+ $extra_values[ $label ] = $value;
+ $i++; // Increment prefix counter for the next extra field
+ }
+
+ $contact_form_subject = trim( $contact_form_subject );
+
+ $comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
+
+ $vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
+ foreach ( $vars as $var ) {
+ $$var = str_replace( array( "\n", "\r" ), '', $$var );
+ }
+
+ // Ensure that Akismet gets all of the relevant information from the contact form,
+ // not just the textarea field and predetermined subject.
+ $akismet_vars = compact( $vars );
+ $akismet_vars['comment_content'] = $comment_content;
+
+ foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
+ $field = $this->fields[ $field_id ];
+
+ // Skip any fields that are just a choice from a pre-defined list. They wouldn't have any value
+ // from a spam-filtering point of view.
+ if ( in_array( $field->get_attribute( 'type' ), array( 'select', 'checkbox', 'checkbox-multiple', 'radio' ) ) ) {
+ continue;
+ }
+
+ // Normalize the label into a slug.
+ $field_slug = trim( // Strip all leading/trailing dashes.
+ preg_replace( // Normalize everything to a-z0-9_-
+ '/[^a-z0-9_]+/',
+ '-',
+ strtolower( $field->get_attribute( 'label' ) ) // Lowercase
+ ),
+ '-'
+ );
+
+ $field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
+
+ // Skip any values that are already in the array we're sending.
+ if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
+ continue;
+ }
+
+ $akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
+ }
+
+ $spam = '';
+ $akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
+
+ // Is it spam?
+ /** This filter is already documented in modules/contact-form/admin.php */
+ $is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
+ if ( is_wp_error( $is_spam ) ) { // WP_Error to abort
+ return $is_spam; // abort
+ } elseif ( $is_spam === true ) { // TRUE to flag a spam
+ $spam = '***SPAM*** ';
+ }
+
+ if ( ! $comment_author ) {
+ $comment_author = $comment_author_email;
+ }
+
+ /**
+ * Filter the email where a submitted feedback is sent.
+ *
+ * @module contact-form
+ *
+ * @since 1.3.1
+ *
+ * @param string|array $to Array of valid email addresses, or single email address.
+ */
+ $to = (array) apply_filters( 'contact_form_to', $to );
+ $reply_to_addr = $to[0]; // get just the address part before the name part is added
+
+ foreach ( $to as $to_key => $to_value ) {
+ $to[ $to_key ] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
+ $to[ $to_key ] = self::add_name_to_address( $to_value );
+ }
+
+ $blog_url = parse_url( site_url() );
+ $from_email_addr = 'wordpress@' . $blog_url['host'];
+
+ if ( ! empty( $comment_author_email ) ) {
+ $reply_to_addr = $comment_author_email;
+ }
+
+ $headers = 'From: "' . $comment_author . '" <' . $from_email_addr . ">\r\n" .
+ 'Reply-To: "' . $comment_author . '" <' . $reply_to_addr . ">\r\n";
+
+ // Build feedback reference
+ $feedback_time = current_time( 'mysql' );
+ $feedback_title = "{$comment_author} - {$feedback_time}";
+ $feedback_id = md5( $feedback_title );
+
+ $all_values = array_merge(
+ $all_values, array(
+ 'entry_title' => the_title_attribute( 'echo=0' ),
+ 'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ),
+ 'feedback_id' => $feedback_id,
+ )
+ );
+
+ /** This filter is already documented in modules/contact-form/admin.php */
+ $subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
+ $url = $widget ? home_url( '/' ) : get_permalink( $post->ID );
+
+ $date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
+ $date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
+ $time = date_i18n( $date_time_format, current_time( 'timestamp' ) );
+
+ // keep a copy of the feedback as a custom post type
+ $feedback_status = $is_spam === true ? 'spam' : 'publish';
+
+ foreach ( (array) $akismet_values as $av_key => $av_value ) {
+ $akismet_values[ $av_key ] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
+ }
+
+ foreach ( (array) $all_values as $all_key => $all_value ) {
+ $all_values[ $all_key ] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
+ }
+
+ foreach ( (array) $extra_values as $ev_key => $ev_value ) {
+ $extra_values[ $ev_key ] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
+ }
+
+ /*
+ We need to make sure that the post author is always zero for contact
+ * form submissions. This prevents export/import from trying to create
+ * new users based on form submissions from people who were logged in
+ * at the time.
+ *
+ * Unfortunately wp_insert_post() tries very hard to make sure the post
+ * author gets the currently logged in user id. That is how we ended up
+ * with this work around. */
+ add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
+
+ $post_id = wp_insert_post(
+ array(
+ 'post_date' => addslashes( $feedback_time ),
+ 'post_type' => 'feedback',
+ 'post_status' => addslashes( $feedback_status ),
+ 'post_parent' => (int) $post->ID,
+ 'post_title' => addslashes( wp_kses( $feedback_title, array() ) ),
+ 'post_content' => addslashes( wp_kses( $comment_content . "\n<!--more-->\n" . "AUTHOR: {$comment_author}\nAUTHOR EMAIL: {$comment_author_email}\nAUTHOR URL: {$comment_author_url}\nSUBJECT: {$subject}\nIP: {$comment_author_IP}\n" . @print_r( $all_values, true ), array() ) ), // so that search will pick up this data
+ 'post_name' => $feedback_id,
+ )
+ );
+
+ // once insert has finished we don't need this filter any more
+ remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10 );
+
+ update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
+
+ if ( 'publish' == $feedback_status ) {
+ // Increase count of unread feedback.
+ $unread = get_option( 'feedback_unread_count', 0 ) + 1;
+ update_option( 'feedback_unread_count', $unread );
+ }
+
+ if ( defined( 'AKISMET_VERSION' ) ) {
+ update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
+ }
+
+ $message = self::get_compiled_form( $post_id, $this );
+
+ array_push(
+ $message,
+ '<br />',
+ '<hr />',
+ __( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
+ __( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
+ __( 'Contact Form URL:', 'jetpack' ) . ' ' . $url . '<br />'
+ );
+
+ if ( is_user_logged_in() ) {
+ array_push(
+ $message,
+ sprintf(
+ '<p>' . __( 'Sent by a verified %s user.', 'jetpack' ) . '</p>',
+ isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
+ $GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
+ )
+ );
+ } else {
+ array_push( $message, '<p>' . __( 'Sent by an unverified visitor to your site.', 'jetpack' ) . '</p>' );
+ }
+
+ $message = join( $message, '' );
+
+ /**
+ * Filters the message sent via email after a successful form submission.
+ *
+ * @module contact-form
+ *
+ * @since 1.3.1
+ *
+ * @param string $message Feedback email message.
+ */
+ $message = apply_filters( 'contact_form_message', $message );
+
+ // This is called after `contact_form_message`, in order to preserve back-compat
+ $message = self::wrap_message_in_html_tags( $message );
+
+ update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
+
+ /**
+ * Fires right before the contact form message is sent via email to
+ * the recipient specified in the contact form.
+ *
+ * @module contact-form
+ *
+ * @since 1.3.1
+ *
+ * @param integer $post_id Post contact form lives on
+ * @param array $all_values Contact form fields
+ * @param array $extra_values Contact form fields not included in $all_values
+ */
+ do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
+
+ // schedule deletes of old spam feedbacks
+ if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
+ wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
+ }
+
+ if (
+ $is_spam !== true &&
+ /**
+ * Filter to choose whether an email should be sent after each successful contact form submission.
+ *
+ * @module contact-form
+ *
+ * @since 2.6.0
+ *
+ * @param bool true Should an email be sent after a form submission. Default to true.
+ * @param int $post_id Post ID.
+ */
+ true === apply_filters( 'grunion_should_send_email', true, $post_id )
+ ) {
+ self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
+ } elseif (
+ true === $is_spam &&
+ /**
+ * Choose whether an email should be sent for each spam contact form submission.
+ *
+ * @module contact-form
+ *
+ * @since 1.3.1
+ *
+ * @param bool false Should an email be sent after a spam form submission. Default to false.
+ */
+ apply_filters( 'grunion_still_email_spam', false ) == true
+ ) { // don't send spam by default. Filterable.
+ self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
+ }
+
+ /**
+ * Fires an action hook right after the email(s) have been sent.
+ *
+ * @module contact-form
+ *
+ * @since 7.3.0
+ *
+ * @param int $post_id Post contact form lives on.
+ * @param string|array $to Array of valid email addresses, or single email address.
+ * @param string $subject Feedback email subject.
+ * @param string $message Feedback email message.
+ * @param string|array $headers Optional. Additional headers.
+ * @param array $all_values Contact form fields.
+ * @param array $extra_values Contact form fields not included in $all_values
+ */
+ do_action( 'grunion_after_message_sent', $post_id, $to, $subject, $message, $headers, $all_values, $extra_values );
+
+ if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
+ return self::success_message( $post_id, $this );
+ }
+
+ $redirect = wp_get_referer();
+ if ( ! $redirect ) { // wp_get_referer() returns false if the referer is the same as the current page
+ $redirect = $_SERVER['REQUEST_URI'];
+ }
+
+ $redirect = add_query_arg(
+ urlencode_deep(
+ array(
+ 'contact-form-id' => $id,
+ 'contact-form-sent' => $post_id,
+ 'contact-form-hash' => $this->hash,
+ '_wpnonce' => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :(
+ )
+ ), $redirect
+ );
+
+ /**
+ * Filter the URL where the reader is redirected after submitting a form.
+ *
+ * @module contact-form
+ *
+ * @since 1.9.0
+ *
+ * @param string $redirect Post submission URL.
+ * @param int $id Contact Form ID.
+ * @param int $post_id Post ID.
+ */
+ $redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
+
+ wp_safe_redirect( $redirect );
+ exit;
+ }
+
+ /**
+ * Wrapper for wp_mail() that enables HTML messages with text alternatives
+ *
+ * @param string|array $to Array or comma-separated list of email addresses to send message.
+ * @param string $subject Email subject.
+ * @param string $message Message contents.
+ * @param string|array $headers Optional. Additional headers.
+ * @param string|array $attachments Optional. Files to attach.
+ *
+ * @return bool Whether the email contents were sent successfully.
+ */
+ public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
+ add_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
+ add_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' );
+
+ $result = wp_mail( $to, $subject, $message, $headers, $attachments );
+
+ remove_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
+ remove_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' );
+
+ return $result;
+ }
+
+ /**
+ * Add a display name part to an email address
+ *
+ * SpamAssassin doesn't like addresses in HTML messages that are missing display names (e.g., `foo@bar.org`
+ * instead of `"Foo Bar" <foo@bar.org>`.
+ *
+ * @param string $address
+ *
+ * @return string
+ */
+ function add_name_to_address( $address ) {
+ // If it's just the address, without a display name
+ if ( is_email( $address ) ) {
+ $address_parts = explode( '@', $address );
+ $address = sprintf( '"%s" <%s>', $address_parts[0], $address );
+ }
+
+ return $address;
+ }
+
+ /**
+ * Get the content type that should be assigned to outbound emails
+ *
+ * @return string
+ */
+ static function get_mail_content_type() {
+ return 'text/html';
+ }
+
+ /**
+ * Wrap a message body with the appropriate in HTML tags
+ *
+ * This helps to ensure correct parsing by clients, and also helps avoid triggering spam filtering rules
+ *
+ * @param string $body
+ *
+ * @return string
+ */
+ static function wrap_message_in_html_tags( $body ) {
+ // Don't do anything if the message was already wrapped in HTML tags
+ // That could have be done by a plugin via filters
+ if ( false !== strpos( $body, '<html' ) ) {
+ return $body;
+ }
+
+ $html_message = sprintf(
+ // The tabs are just here so that the raw code is correctly formatted for developers
+ // They're removed so that they don't affect the final message sent to users
+ str_replace(
+ "\t", '',
+ '<!doctype html>
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <body>
+
+ %s
+
+ </body>
+ </html>'
+ ),
+ $body
+ );
+
+ return $html_message;
+ }
+
+ /**
+ * Add a plain-text alternative part to an outbound email
+ *
+ * This makes the message more accessible to mail clients that aren't HTML-aware, and decreases the likelihood
+ * that the message will be flagged as spam.
+ *
+ * @param PHPMailer $phpmailer
+ */
+ static function add_plain_text_alternative( $phpmailer ) {
+ // Add an extra break so that the extra space above the <p> is preserved after the <p> is stripped out
+ $alt_body = str_replace( '<p>', '<p><br />', $phpmailer->Body );
+
+ // Convert <br> to \n breaks, to preserve the space between lines that we want to keep
+ $alt_body = str_replace( array( '<br>', '<br />' ), "\n", $alt_body );
+
+ // Convert <hr> to an plain-text equivalent, to preserve the integrity of the message
+ $alt_body = str_replace( array( '<hr>', '<hr />' ), "----\n", $alt_body );
+
+ // Trim the plain text message to remove the \n breaks that were after <doctype>, <html>, and <body>
+ $phpmailer->AltBody = trim( strip_tags( $alt_body ) );
+ }
+
+ function addslashes_deep( $value ) {
+ if ( is_array( $value ) ) {
+ return array_map( array( $this, 'addslashes_deep' ), $value );
+ } elseif ( is_object( $value ) ) {
+ $vars = get_object_vars( $value );
+ foreach ( $vars as $key => $data ) {
+ $value->{$key} = $this->addslashes_deep( $data );
+ }
+ return $value;
+ }
+
+ return addslashes( $value );
+ }
+}
+
+/**
+ * Class for the contact-field shortcode.
+ * Parses shortcode to output the contact form field as HTML.
+ * Validates input.
+ */
+class Grunion_Contact_Form_Field extends Crunion_Contact_Form_Shortcode {
+ public $shortcode_name = 'contact-field';
+
+ /**
+ * @var Grunion_Contact_Form parent form
+ */
+ public $form;
+
+ /**
+ * @var string default or POSTed value
+ */
+ public $value;
+
+ /**
+ * @var bool Is the input invalid?
+ */
+ public $error = false;
+
+ /**
+ * @param array $attributes An associative array of shortcode attributes. @see shortcode_atts()
+ * @param null|string $content Null for selfclosing shortcodes. The inner content otherwise.
+ * @param Grunion_Contact_Form $form The parent form
+ */
+ function __construct( $attributes, $content = null, $form = null ) {
+ $attributes = shortcode_atts(
+ array(
+ 'label' => null,
+ 'type' => 'text',
+ 'required' => false,
+ 'options' => array(),
+ 'id' => null,
+ 'default' => null,
+ 'values' => null,
+ 'placeholder' => null,
+ 'class' => null,
+ ), $attributes, 'contact-field'
+ );
+
+ // special default for subject field
+ if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && ! is_null( $form ) ) {
+ $attributes['default'] = $form->get_attribute( 'subject' );
+ }
+
+ // allow required=1 or required=true
+ if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) ) {
+ $attributes['required'] = true;
+ } else {
+ $attributes['required'] = false;
+ }
+
+ // parse out comma-separated options list (for selects, radios, and checkbox-multiples)
+ if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
+ $attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
+
+ if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) {
+ $attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) );
+ }
+ }
+
+ if ( $form ) {
+ // make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
+ $form_id = $form->get_attribute( 'id' );
+ $id = isset( $attributes['id'] ) ? $attributes['id'] : false;
+
+ $unescaped_label = $this->unesc_attr( $attributes['label'] );
+ $unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
+ $unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
+
+ if ( empty( $id ) ) {
+ $id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
+ $i = 0;
+ $max_tries = 99;
+ while ( isset( $form->fields[ $id ] ) ) {
+ $i++;
+ $id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
+
+ if ( $i > $max_tries ) {
+ break;
+ }
+ }
+ }
+
+ $attributes['id'] = $id;
+ }
+
+ parent::__construct( $attributes, $content );
+
+ // Store parent form
+ $this->form = $form;
+ }
+
+ /**
+ * This field's input is invalid. Flag as invalid and add an error to the parent form
+ *
+ * @param string $message The error message to display on the form.
+ */
+ function add_error( $message ) {
+ $this->is_error = true;
+
+ if ( ! is_wp_error( $this->form->errors ) ) {
+ $this->form->errors = new WP_Error;
+ }
+
+ $this->form->errors->add( $this->get_attribute( 'id' ), $message );
+ }
+
+ /**
+ * Is the field input invalid?
+ *
+ * @see $error
+ *
+ * @return bool
+ */
+ function is_error() {
+ return $this->error;
+ }
+
+ /**
+ * Validates the form input
+ */
+ function validate() {
+ // If it's not required, there's nothing to validate
+ if ( ! $this->get_attribute( 'required' ) ) {
+ return;
+ }
+
+ $field_id = $this->get_attribute( 'id' );
+ $field_type = $this->get_attribute( 'type' );
+ $field_label = $this->get_attribute( 'label' );
+
+ if ( isset( $_POST[ $field_id ] ) ) {
+ if ( is_array( $_POST[ $field_id ] ) ) {
+ $field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
+ } else {
+ $field_value = stripslashes( $_POST[ $field_id ] );
+ }
+ } else {
+ $field_value = '';
+ }
+
+ switch ( $field_type ) {
+ case 'email':
+ // Make sure the email address is valid
+ if ( ! is_email( $field_value ) ) {
+ /* translators: %s is the name of a form field */
+ $this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
+ }
+ break;
+ case 'checkbox-multiple':
+ // Check that there is at least one option selected
+ if ( empty( $field_value ) ) {
+ /* translators: %s is the name of a form field */
+ $this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
+ }
+ break;
+ default:
+ // Just check for presence of any text
+ if ( ! strlen( trim( $field_value ) ) ) {
+ /* translators: %s is the name of a form field */
+ $this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
+ }
+ }
+ }
+
+
+ /**
+ * Check the default value for options field
+ *
+ * @param string value
+ * @param int index
+ * @param string default value
+ *
+ * @return string
+ */
+ public function get_option_value( $value, $index, $options ) {
+ if ( empty( $value[ $index ] ) ) {
+ return $options;
+ }
+ return $value[ $index ];
+ }
+
+ /**
+ * Outputs the HTML for this form field
+ *
+ * @return string HTML
+ */
+ function render() {
+ global $current_user, $user_identity;
+
+ $field_id = $this->get_attribute( 'id' );
+ $field_type = $this->get_attribute( 'type' );
+ $field_label = $this->get_attribute( 'label' );
+ $field_required = $this->get_attribute( 'required' );
+ $field_placeholder = $this->get_attribute( 'placeholder' );
+ $class = 'date' === $field_type ? 'jp-contact-form-date' : $this->get_attribute( 'class' );
+
+ /**
+ * Filters the "class" attribute of the contact form input
+ *
+ * @module contact-form
+ *
+ * @since 6.6.0
+ *
+ * @param string $class Additional CSS classes for input class attribute.
+ */
+ $field_class = apply_filters( 'jetpack_contact_form_input_class', $class );
+
+ if ( isset( $_POST[ $field_id ] ) ) {
+ if ( is_array( $_POST[ $field_id ] ) ) {
+ $this->value = array_map( 'stripslashes', $_POST[ $field_id ] );
+ } else {
+ $this->value = stripslashes( (string) $_POST[ $field_id ] );
+ }
+ } elseif ( isset( $_GET[ $field_id ] ) ) {
+ $this->value = stripslashes( (string) $_GET[ $field_id ] );
+ } elseif (
+ is_user_logged_in() &&
+ ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
+ /**
+ * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
+ *
+ * @module contact-form
+ *
+ * @since 3.2.0
+ *
+ * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
+ */
+ true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
+ )
+ ) {
+ // Special defaults for logged-in users
+ switch ( $this->get_attribute( 'type' ) ) {
+ case 'email':
+ $this->value = $current_user->data->user_email;
+ break;
+ case 'name':
+ $this->value = $user_identity;
+ break;
+ case 'url':
+ $this->value = $current_user->data->user_url;
+ break;
+ default:
+ $this->value = $this->get_attribute( 'default' );
+ }
+ } else {
+ $this->value = $this->get_attribute( 'default' );
+ }
+
+ $field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
+ $field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
+
+ $rendered_field = $this->render_field( $field_type, $field_id, $field_label, $field_value, $field_class, $field_placeholder, $field_required );
+
+ /**
+ * Filter the HTML of the Contact Form.
+ *
+ * @module contact-form
+ *
+ * @since 2.6.0
+ *
+ * @param string $rendered_field Contact Form HTML output.
+ * @param string $field_label Field label.
+ * @param int|null $id Post ID.
+ */
+ return apply_filters( 'grunion_contact_form_field_html', $rendered_field, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
+ }
+
+ function render_label( $type = '', $id, $label, $required, $required_field_text ) {
+
+ $type_class = $type ? ' ' .$type : '';
+ return
+ "<label
+ for='" . esc_attr( $id ) . "'
+ class='grunion-field-label{$type_class}" . ( $this->is_error() ? ' form-error' : '' ) . "'
+ >"
+ . esc_html( $label )
+ . ( $required ? '<span>' . $required_field_text . '</span>' : '' )
+ . "</label>\n";
+
+ }
+
+ function render_input_field( $type, $id, $value, $class, $placeholder, $required ) {
+ return "<input
+ type='". esc_attr( $type ) ."'
+ name='" . esc_attr( $id ) . "'
+ id='" . esc_attr( $id ) . "'
+ value='" . esc_attr( $value ) . "'
+ " . $class . $placeholder . '
+ ' . ( $required ? "required aria-required='true'" : '' ) . "
+ />\n";
+ }
+
+ function render_email_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
+ $field = $this->render_label( 'email', $id, $label, $required, $required_field_text );
+ $field .= $this->render_input_field( 'email', $id, $value, $class, $placeholder, $required );
+ return $field;
+ }
+
+ function render_telephone_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
+ $field = $this->render_label( 'telephone', $id, $label, $required, $required_field_text );
+ $field .= $this->render_input_field( 'tel', $id, $value, $class, $placeholder, $required );
+ return $field;
+ }
+
+ function render_url_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
+ $field = $this->render_label( 'url', $id, $label, $required, $required_field_text );
+ $field .= $this->render_input_field( 'url', $id, $value, $class, $placeholder, $required );
+ return $field;
+ }
+
+ function render_textarea_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
+ $field = $this->render_label( 'textarea', 'contact-form-comment-' . $id, $label, $required, $required_field_text );
+ $field .= "<textarea
+ name='" . esc_attr( $id ) . "'
+ id='contact-form-comment-" . esc_attr( $id ) . "'
+ rows='20' "
+ . $class
+ . $placeholder
+ . ' ' . ( $required ? "required aria-required='true'" : '' ) .
+ '>' . esc_textarea( $value )
+ . "</textarea>\n";
+ return $field;
+ }
+
+ function render_radio_field( $id, $label, $value, $class, $required, $required_field_text ) {
+ $field = $this->render_label( '', $id, $label, $required, $required_field_text );
+ foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
+ $option = Grunion_Contact_Form_Plugin::strip_tags( $option );
+ if ( $option ) {
+ $field .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
+ $field .= "<input
+ type='radio'
+ name='" . esc_attr( $id ) . "'
+ value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' "
+ . $class
+ . checked( $option, $value, false ) . ' '
+ . ( $required ? "required aria-required='true'" : '' )
+ . '/> ';
+ $field .= esc_html( $option ) . "</label>\n";
+ $field .= "\t\t<div class='clear-form'></div>\n";
+ }
+ }
+ return $field;
+ }
+
+ function render_checkbox_field( $id, $label, $value, $class, $required, $required_field_text ) {
+ $field = "<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
+ $field .= "\t\t<input type='checkbox' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack' ) . "' " . $class . checked( (bool) $value, true, false ) . ' ' . ( $required ? "required aria-required='true'" : '' ) . "/> \n";
+ $field .= "\t\t" . esc_html( $label ) . ( $required ? '<span>' . $required_field_text . '</span>' : '' );
+ $field .= "</label>\n";
+ $field .= "<div class='clear-form'></div>\n";
+ return $field;
+ }
+
+ function render_checkbox_multiple_field( $id, $label, $value, $class, $required, $required_field_text ) {
+ $field = $this->render_label( '', $id, $label, $required, $required_field_text );
+ foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
+ $option = Grunion_Contact_Form_Plugin::strip_tags( $option );
+ if ( $option ) {
+ $field .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
+ $field .= "<input type='checkbox' name='" . esc_attr( $id ) . "[]' value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' " . $class . checked( in_array( $option, (array) $value ), true, false ) . ' /> ';
+ $field .= esc_html( $option ) . "</label>\n";
+ $field .= "\t\t<div class='clear-form'></div>\n";
+ }
+ }
+
+ return $field;
+ }
+
+ function render_select_field( $id, $label, $value, $class, $required, $required_field_text ) {
+ $field = $this->render_label( 'select', $id, $label, $required, $required_field_text );
+ $field .= "\t<select name='" . esc_attr( $id ) . "' id='" . esc_attr( $id ) . "' " . $class . ( $required ? "required aria-required='true'" : '' ) . ">\n";
+ foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
+ $option = Grunion_Contact_Form_Plugin::strip_tags( $option );
+ if ( $option ) {
+ $field .= "\t\t<option"
+ . selected( $option, $value, false )
+ . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) )
+ . "'>" . esc_html( $option )
+ . "</option>\n";
+ }
+ }
+ $field .= "\t</select>\n";
+ return $field;
+ }
+
+ function render_date_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
+ $field = $this->render_label( 'date', $id, $label, $required, $required_field_text );
+ $field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
+
+ wp_enqueue_script(
+ 'grunion-frontend',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/contact-form/js/grunion-frontend.min.js',
+ 'modules/contact-form/js/grunion-frontend.js'
+ ),
+ array( 'jquery', 'jquery-ui-datepicker' )
+ );
+ wp_enqueue_style( 'jp-jquery-ui-datepicker', plugins_url( 'css/jquery-ui-datepicker.css', __FILE__ ), array( 'dashicons' ), '1.0' );
+
+ // Using Core's built-in datepicker localization routine
+ wp_localize_jquery_ui_datepicker();
+ return $field;
+ }
+
+ function render_default_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $type ) {
+ $field = $this->render_label( $type, $id, $label, $required, $required_field_text );
+ $field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
+ return $field;
+ }
+
+ function render_field( $type, $id, $label, $value, $class, $placeholder, $required ) {
+
+ $field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
+ $field_class = "class='" . trim( esc_attr( $type ) . ' ' . esc_attr( $class ) ) . "' ";
+ $wrap_classes = empty( $class ) ? '' : implode( '-wrap ', array_filter( explode( ' ', $class ) ) ) . '-wrap'; // this adds
+
+ $shell_field_class = "class='grunion-field-wrap grunion-field-" . trim( esc_attr( $type ) . '-wrap ' . esc_attr( $wrap_classes ) ) . "' ";
+ /**
+ /**
+ * Filter the Contact Form required field text
+ *
+ * @module contact-form
+ *
+ * @since 3.8.0
+ *
+ * @param string $var Required field text. Default is "(required)".
+ */
+ $required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ) );
+
+ $field = "\n<div {$shell_field_class} >\n"; // new in Jetpack 6.8.0
+ // If they are logged in, and this is their site, don't pre-populate fields
+ if ( current_user_can( 'manage_options' ) ) {
+ $value = '';
+ }
+ switch ( $type ) {
+ case 'email':
+ $field .= $this->render_email_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
+ break;
+ case 'telephone':
+ $field .= $this->render_telephone_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
+ break;
+ case 'url':
+ $field .= $this->render_url_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
+ break;
+ case 'textarea':
+ $field .= $this->render_textarea_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
+ break;
+ case 'radio':
+ $field .= $this->render_radio_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
+ break;
+ case 'checkbox':
+ $field .= $this->render_checkbox_field( $id, $label, $value, $field_class, $required, $required_field_text );
+ break;
+ case 'checkbox-multiple':
+ $field .= $this->render_checkbox_multiple_field( $id, $label, $value, $field_class, $required, $required_field_text );
+ break;
+ case 'select':
+ $field .= $this->render_select_field( $id, $label, $value, $field_class, $required, $required_field_text );
+ break;
+ case 'date':
+ $field .= $this->render_date_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
+ break;
+ default: // text field
+ $field .= $this->render_default_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $type );
+ break;
+ }
+ $field .= "\t</div>\n";
+ return $field;
+ }
+}
+
+add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ), 9 );
+
+add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
+
+/**
+ * Deletes old spam feedbacks to keep the posts table size under control
+ */
+function grunion_delete_old_spam() {
+ global $wpdb;
+
+ $grunion_delete_limit = 100;
+
+ $now_gmt = current_time( 'mysql', 1 );
+ $sql = $wpdb->prepare(
+ "
+ SELECT `ID`
+ FROM $wpdb->posts
+ WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
+ AND `post_type` = 'feedback'
+ AND `post_status` = 'spam'
+ LIMIT %d
+ ", $now_gmt, $grunion_delete_limit
+ );
+ $post_ids = $wpdb->get_col( $sql );
+
+ foreach ( (array) $post_ids as $post_id ) {
+ // force a full delete, skip the trash
+ wp_delete_post( $post_id, true );
+ }
+
+ if (
+ /**
+ * Filter if the module run OPTIMIZE TABLE on the core WP tables.
+ *
+ * @module contact-form
+ *
+ * @since 1.3.1
+ * @since 6.4.0 Set to false by default.
+ *
+ * @param bool $filter Should Jetpack optimize the table, defaults to false.
+ */
+ apply_filters( 'grunion_optimize_table', false )
+ ) {
+ $wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
+ }
+
+ // if we hit the max then schedule another run
+ if ( count( $post_ids ) >= $grunion_delete_limit ) {
+ wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
+ }
+}
diff --git a/plugins/jetpack/modules/contact-form/grunion-editor-view.php b/plugins/jetpack/modules/contact-form/grunion-editor-view.php
new file mode 100644
index 00000000..d1ba0439
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/grunion-editor-view.php
@@ -0,0 +1,299 @@
+<?php
+
+/*
+ * A prototype to allow inline editing / editor views for contact forms.\
+ *
+ * Originally developed in: http://github.com/automattic/gm2016-grunion-editor
+ * Authors: Michael Arestad, Andrew Ozz, and George Stephanis
+ */
+
+class Grunion_Editor_View {
+
+ /**
+ * Add hooks according to screen.
+ *
+ * @param WP_Screen $screen Data about current screen.
+ */
+ public static function add_hooks( $screen ) {
+ if ( isset( $screen->base ) && 'post' === $screen->base ) {
+ add_action( 'admin_notices', array( __CLASS__, 'handle_editor_view_js' ) );
+ add_action( 'admin_head', array( __CLASS__, 'admin_head' ) );
+ }
+ }
+
+ public static function admin_head() {
+ remove_action( 'media_buttons', 'grunion_media_button', 999 );
+ add_action( 'media_buttons', array( __CLASS__, 'grunion_media_button' ), 999 );
+ }
+
+ public static function grunion_media_button() {
+ $title = __( 'Add Contact Form', 'jetpack' );
+ ?>
+
+ <button type="button" id="insert-jetpack-contact-form" class="button" title="<?php echo esc_attr( $title ); ?>" href="javascript:;">
+ <span class="jetpack-contact-form-icon"></span>
+ <?php echo esc_html( $title ); ?>
+ </button>
+
+ <?php
+ }
+
+ public static function mce_external_plugins( $plugin_array ) {
+ $plugin_array['grunion_form'] = Jetpack::get_file_url_for_environment(
+ '_inc/build/contact-form/js/tinymce-plugin-form-button.min.js',
+ 'modules/contact-form/js/tinymce-plugin-form-button.js'
+ );
+ return $plugin_array;
+ }
+
+ public static function mce_buttons( $buttons ) {
+ $size = sizeof( $buttons );
+ $buttons1 = array_slice( $buttons, 0, $size - 1 );
+ $buttons2 = array_slice( $buttons, $size - 1 );
+ return array_merge(
+ $buttons1,
+ array( 'grunion' ),
+ $buttons2
+ );
+ }
+
+ /**
+ * WordPress Shortcode Editor View JS Code
+ */
+ public static function handle_editor_view_js() {
+ add_action( 'admin_print_footer_scripts', array( __CLASS__, 'editor_view_js_templates' ), 1 );
+ add_filter( 'mce_external_plugins', array( __CLASS__, 'mce_external_plugins' ) );
+ add_filter( 'mce_buttons', array( __CLASS__, 'mce_buttons' ) );
+
+ wp_enqueue_style( 'grunion-editor-ui', plugins_url( 'css/editor-ui.css', __FILE__ ) );
+ wp_style_add_data( 'grunion-editor-ui', 'rtl', 'replace' );
+ wp_enqueue_script(
+ 'grunion-editor-view',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/contact-form/js/editor-view.min.js',
+ 'modules/contact-form/js/editor-view.js'
+ ),
+ array( 'wp-util', 'jquery', 'quicktags' ),
+ false,
+ true
+ );
+ wp_localize_script(
+ 'grunion-editor-view', 'grunionEditorView', array(
+ 'inline_editing_style' => plugins_url( 'css/editor-inline-editing-style.css', __FILE__ ),
+ 'inline_editing_style_rtl' => plugins_url( 'css/editor-inline-editing-style-rtl.css', __FILE__ ),
+ 'dashicons_css_url' => includes_url( 'css/dashicons.css' ),
+ 'default_form' => '[contact-field label="' . __( 'Name', 'jetpack' ) . '" type="name" required="true" /]' .
+ '[contact-field label="' . __( 'Email', 'jetpack' ) . '" type="email" required="true" /]' .
+ '[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]' .
+ '[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]',
+ 'labels' => array(
+ 'submit_button_text' => __( 'Submit', 'jetpack' ),
+ /** This filter is documented in modules/contact-form/grunion-contact-form.php */
+ 'required_field_text' => apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ),
+ 'edit_close_ays' => __( 'Are you sure you\'d like to stop editing this form without saving your changes?', 'jetpack' ),
+ 'quicktags_label' => __( 'contact form', 'jetpack' ),
+ 'tinymce_label' => __( 'Add contact form', 'jetpack' ),
+ ),
+ )
+ );
+
+ add_editor_style( plugin_dir_url( __FILE__ ) . 'css/editor-style.css' );
+ }
+
+ /**
+ * JS Templates.
+ */
+ public static function editor_view_js_templates() {
+ ?>
+<script type="text/html" id="tmpl-grunion-contact-form">
+ <form class="card jetpack-contact-form-shortcode-preview" action='#' method='post' class='contact-form commentsblock' onsubmit="return false;">
+ {{{ data.body }}}
+ <p class='contact-submit'>
+ <input type='submit' value='{{ data.submit_button_text }}' class='pushbutton-wide'/>
+ </p>
+ </form>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-email">
+ <div>
+ <label for='{{ data.id }}' class='grunion-field-label email'>{{ data.label }}<# if ( data.required ) print( " <span>" + data.required + "</span>" ) #></label>
+ <input type='email' name='{{ data.id }}' id='{{ data.id }}' value='{{ data.value }}' class='{{ data.class }}' placeholder='{{ data.placeholder }}' />
+ </div>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-telephone">
+ <div>
+ <label for='{{ data.id }}' class='grunion-field-label telephone'>{{ data.label }}<# if ( data.required ) print( " <span>" + data.required + "</span>" ) #></label>
+ <input type='tel' name='{{ data.id }}' id='{{ data.id }}' value='{{ data.value }}' class='{{ data.class }}' placeholder='{{ data.placeholder }}' />
+ </div>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-textarea">
+ <div>
+ <label for='contact-form-comment-{{ data.id }}' class='grunion-field-label textarea'>{{ data.label }}<# if ( data.required ) print( " <span>" + data.required + "</span>" ) #></label>
+ <textarea name='{{ data.id }}' id='contact-form-comment-{{ data.id }}' rows='20' class='{{ data.class }}' placeholder='{{ data.placeholder }}'>{{ data.value }}</textarea>
+ </div>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-radio">
+ <div>
+ <label class='grunion-field-label'>{{ data.label }}<# if ( data.required ) print( " <span>" + data.required + "</span>" ) #></label>
+ <# _.each( data.options, function( option ) { #>
+ <label class='grunion-radio-label radio'>
+ <input type='radio' name='{{ data.id }}' value='{{ option }}' class="{{ data.class }}" <# if ( option === data.value ) print( "checked='checked'" ) #> />
+ <span>{{ option }}</span>
+ </label>
+ <# }); #>
+ <div class='clear-form'></div>
+ </div>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-checkbox">
+ <div>
+ <label class='grunion-field-label checkbox'>
+ <input type='checkbox' name='{{ data.id }}' value='<?php esc_attr__( 'Yes', 'jetpack' ); ?>' class="{{ data.class }}" <# if ( data.value ) print( 'checked="checked"' ) #> />
+ <span>{{ data.label }}</span><# if ( data.required ) print( " <span>" + data.required + "</span>" ) #>
+ </label>
+ <div class='clear-form'></div>
+ </div>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-checkbox-multiple">
+ <div>
+ <label class='grunion-field-label'>{{ data.label }}<# if ( data.required ) print( " <span>" + data.required + "</span>" ) #></label>
+ <# _.each( data.options, function( option ) { #>
+ <label class='grunion-checkbox-multiple-label checkbox-multiple'>
+ <input type='checkbox' name='{{ data.id }}[]' value='{{ option }}' class="{{ data.class }}" <# if ( option === data.value || _.contains( data.value, option ) ) print( "checked='checked'" ) #> />
+ <span>{{ option }}</span>
+ </label>
+ <# }); #>
+ <div class='clear-form'></div>
+ </div>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-select">
+ <div>
+ <label for='{{ data.id }}' class='grunion-field-label select'>{{ data.label }}<# if ( data.required ) print( " <span>" + data.required + "</span>" ) #></label>
+ <select name='{{ data.id }}' id='{{ data.id }}' class="{{ data.class }}">
+ <# _.each( data.options, function( option ) { #>
+ <option <# if ( option === data.value ) print( "selected='selected'" ) #>>{{ option }}</option>
+ <# }); #>
+ </select>
+ </div>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-date">
+ <div>
+ <label for='{{ data.id }}' class='grunion-field-label {{ data.type }}'>{{ data.label }}<# if ( data.required ) print( " <span>" + data.required + "</span>" ) #></label>
+ <input type='text' name='{{ data.id }}' id='{{ data.id }}' value='{{ data.value }}' class="{{ data.class }}" />
+ </div>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-text">
+ <div>
+ <label for='{{ data.id }}' class='grunion-field-label {{ data.type }}'>{{ data.label }}<# if ( data.required ) print( " <span>" + data.required + "</span>" ) #></label>
+ <input type='text' name='{{ data.id }}' id='{{ data.id }}' value='{{ data.value }}' class='{{ data.class }}' placeholder='{{ data.placeholder }}' />
+ </div>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-url">
+ <div>
+ <label for='{{ data.id }}' class='grunion-field-label {{ data.type }}'>{{ data.label }}<# if ( data.required ) print( " <span>" + data.required + "</span>" ) #></label>
+ <input type='url' name='{{ data.id }}' id='{{ data.id }}' value='{{ data.value }}' class='{{ data.class }}' placeholder='{{ data.placeholder }}' />
+ </div>
+</script>
+
+
+<script type="text/html" id="tmpl-grunion-field-edit">
+ <div class="card is-compact grunion-field-edit grunion-field-{{ data.type }}" aria-label="<?php esc_attr_e( 'Form Field', 'jetpack' ); ?>">
+ <label class="grunion-name">
+ <span><?php esc_html_e( 'Field Label', 'jetpack' ); ?></span>
+ <input type="text" name="label" placeholder="<?php esc_attr_e( 'Label', 'jetpack' ); ?>" value="{{ data.label }}"/>
+ </label>
+
+ <?php
+ $grunion_field_types = array(
+ 'text' => __( 'Text', 'jetpack' ),
+ 'name' => __( 'Name', 'jetpack' ),
+ 'email' => __( 'Email', 'jetpack' ),
+ 'url' => __( 'Website', 'jetpack' ),
+ 'textarea' => __( 'Textarea', 'jetpack' ),
+ 'checkbox' => __( 'Checkbox', 'jetpack' ),
+ 'checkbox-multiple' => __( 'Checkbox with Multiple Items', 'jetpack' ),
+ 'select' => __( 'Drop down', 'jetpack' ),
+ 'radio' => __( 'Radio', 'jetpack' ),
+ 'date' => __( 'Date', 'jetpack' ),
+ );
+ ?>
+ <div class="grunion-type-options">
+ <label class="grunion-type">
+ <?php esc_html_e( 'Field Type', 'jetpack' ); ?>
+ <select name="type">
+ <?php foreach ( $grunion_field_types as $type => $label ) : ?>
+ <option <# if ( '<?php echo esc_js( $type ); ?>' === data.type ) print( "selected='selected'" ) #> value="<?php echo esc_attr( $type ); ?>">
+ <?php echo esc_html( $label ); ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </label>
+
+ <label class="grunion-required">
+ <input type="checkbox" name="required" value="1" <# if ( data.required ) print( 'checked="checked"' ) #> />
+ <span><?php esc_html_e( 'Required?', 'jetpack' ); ?></span>
+ </label>
+ </div>
+
+ <label class="grunion-options">
+ <?php esc_html_e( 'Options', 'jetpack' ); ?>
+ <ol>
+ <# if ( data.options ) { #>
+ <# _.each( data.options, function( option ) { #>
+ <li><input type="text" name="option" value="{{ option }}" /> <a class="delete-option" href="javascript:;"><span class="screen-reader-text"><?php esc_html_e( 'Delete Option', 'jetpack' ); ?></span></a></li>
+ <# }); #>
+ <# } else { #>
+ <li><input type="text" name="option" /> <a class="delete-option" href="javascript:;"><span class="screen-reader-text"><?php esc_html_e( 'Delete Option', 'jetpack' ); ?></span></a></li>
+ <li><input type="text" name="option" /> <a class="delete-option" href="javascript:;"><span class="screen-reader-text"><?php esc_html_e( 'Delete Option', 'jetpack' ); ?></span></a></li>
+ <li><input type="text" name="option" /> <a class="delete-option" href="javascript:;"><span class="screen-reader-text"><?php esc_html_e( 'Delete Option', 'jetpack' ); ?></span></a></li>
+ <# } #>
+ <li><a class="add-option" href="javascript:;"><?php esc_html_e( 'Add new option...', 'jetpack' ); ?></a></li>
+ </ol>
+ </label>
+
+ <a href="javascript:;" class="delete-field"><span class="screen-reader-text"><?php esc_html_e( 'Delete Field', 'jetpack' ); ?></span></a>
+ </div>
+</script>
+
+<script type="text/html" id="tmpl-grunion-field-edit-option">
+ <li><input type="text" name="option" /> <a class="delete-option" href="javascript:;"><span class="screen-reader-text"><?php esc_html_e( 'Delete Option', 'jetpack' ); ?></span></a></li>
+</script>
+
+<script type="text/html" id="tmpl-grunion-editor-inline">
+ <h1 id="form-settings-header" class="grunion-section-header"><?php esc_html_e( 'Contact form information', 'jetpack' ); ?></h1>
+ <section class="card grunion-form-settings" aria-labelledby="form-settings-header">
+ <label><?php esc_html_e( 'What would you like the subject of the email to be?', 'jetpack' ); ?>
+ <input type="text" name="subject" value="{{ data.subject }}" />
+ </label>
+ <label><?php esc_html_e( 'Which email address should we send the submissions to?', 'jetpack' ); ?>
+ <input type="text" name="to" value="{{ data.to }}" />
+ </label>
+ </section>
+ <h1 id="form-fields-header" class="grunion-section-header"><?php esc_html_e( 'Contact form fields', 'jetpack' ); ?></h1>
+ <section class="grunion-fields" aria-labelledby="form-fields-header">
+ {{{ data.fields }}}
+ </section>
+ <section class="grunion-controls">
+ <?php submit_button( esc_html__( 'Add Field', 'jetpack' ), 'secondary', 'add-field', false ); ?>
+
+ <div class="grunion-update-controls">
+ <?php submit_button( esc_html__( 'Cancel', 'jetpack' ), 'delete', 'cancel', false ); ?>
+ <?php submit_button( esc_html__( 'Update Form', 'jetpack' ), 'primary', 'submit', false ); ?>
+ </div>
+ </section>
+</script>
+
+</div>
+ <?php
+ }
+}
+
+add_action( 'current_screen', array( 'Grunion_Editor_View', 'add_hooks' ) );
diff --git a/plugins/jetpack/modules/contact-form/grunion-form-view.php b/plugins/jetpack/modules/contact-form/grunion-form-view.php
new file mode 100644
index 00000000..26ed6dfe
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/grunion-form-view.php
@@ -0,0 +1,266 @@
+<?php
+/**
+ * Template for form builder
+ */
+
+/**
+ * Filter to modify the limit of 5 additional contact form fields.
+ *
+ * @module contact-form
+ *
+ * @since 3.2.0
+ *
+ * @param int 5 Maximum number of additional fields.
+ */
+$max_new_fields = apply_filters( 'grunion_max_new_fields', 5 );
+
+wp_register_script(
+ 'grunion',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/contact-form/js/grunion.min.js',
+ 'modules/contact-form/js/grunion.js'
+ ),
+ array( 'jquery-ui-sortable', 'jquery-ui-draggable' ),
+ JETPACK__VERSION
+);
+
+wp_localize_script(
+ 'grunion', 'GrunionFB_i18n', array(
+ 'nameLabel' => esc_attr( _x( 'Name', 'Label for HTML form "Name" field in contact form builder', 'jetpack' ) ),
+ 'emailLabel' => esc_attr( _x( 'Email', 'Label for HTML form "Email" field in contact form builder', 'jetpack' ) ),
+ 'urlLabel' => esc_attr( _x( 'Website', 'Label for HTML form "URL/Website" field in contact form builder', 'jetpack' ) ),
+ 'commentLabel' => esc_attr( _x( 'Comment', 'noun', 'jetpack' ) ),
+ 'newLabel' => esc_attr( _x( 'New Field', 'Default label for new HTML form field in contact form builder', 'jetpack' ) ),
+ 'optionsLabel' => esc_attr( _x( 'Options', 'Label for the set of options to be included in a user-created dropdown in contact form builder', 'jetpack' ) ),
+ 'optionsLabel' => esc_attr( _x( 'Option', 'Label for an option to be included in a user-created dropdown in contact form builder', 'jetpack' ) ),
+ 'firstOptionLabel' => esc_attr( _x( 'First option', 'Default label for the first option to be included in a user-created dropdown in contact form builder', 'jetpack' ) ),
+ 'problemGeneratingForm' => esc_attr( _x( "Oops, there was a problem generating your form. You'll likely need to try again.", 'error message in contact form builder', 'jetpack' ) ),
+ 'moveInstructions' => esc_attr__( "Drag up or down\nto re-arrange", 'jetpack' ),
+ 'moveLabel' => esc_attr( _x( 'move', 'Label to drag HTML form fields around to change their order in contact form builder', 'jetpack' ) ),
+ 'editLabel' => esc_attr( _x( 'edit', 'Link to edit an HTML form field in contact form builder', 'jetpack' ) ),
+ 'savedMessage' => esc_attr__( 'Saved successfully', 'jetpack' ),
+ 'requiredLabel' => esc_attr( _x( '(required)', 'This HTML form field is marked as required by the user in contact form builder', 'jetpack' ) ),
+ 'exitConfirmMessage' => esc_attr__( 'Are you sure you want to exit the form editor without saving? Any changes you have made will be lost.', 'jetpack' ),
+ 'maxNewFields' => intval( $max_new_fields ),
+ )
+);
+
+?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title><?php esc_html_e( 'Contact Form', 'jetpack' ); ?></title>
+<script type="text/javascript">
+ var ajaxurl = '<?php echo admin_url( 'admin-ajax.php' ); ?>';
+ var postId = <?php echo absint( $_GET['post_id'] ); ?>;
+ var ajax_nonce_shortcode = '<?php echo wp_create_nonce( 'grunion_shortcode' ); ?>';
+ var ajax_nonce_json = '<?php echo wp_create_nonce( 'grunion_shortcode_to_json' ); ?>';
+</script>
+<?php wp_print_scripts( 'grunion' ); ?>
+<script type="text/javascript">
+ jQuery(document).ready(function () {
+ FB.ContactForm.init();
+ FB.ContactForm.resizePop();
+ });
+ jQuery(window).resize(function() {
+ setTimeout(function () { FB.ContactForm.resizePop(); }, 50);
+ });
+</script>
+<style>
+ /* Reset */
+ html { height: 100%; }
+ body, div, ul, ol, li, h1, h2, h3, h4, h5, h6, form, fieldset, legend, input, button, textarea, p, blockquote, th, td { margin: 0; padding: 0; }
+ body { background: #F9F9F9; font-family:"Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif; font-size:12px; color: #333; line-height:1.5em; height: 100%; width: 100%; padding-bottom: 20px !important; }
+ a { color: #21759B; text-decoration: none; }
+ a:hover { text-decoration: underline; text-shadow: none !important; }
+ h1 { font-size: 21px; color:#5A5A5A; font-family:Georgia,"Times New Roman",Times,serif; font-weight:normal; margin-bottom: 21px; }
+ h3 { font-size: 13px; color: #666; margin-bottom: 18px; }
+ input { width: 301px; }
+ input[type='text'] { padding: 3px 5px; margin-right: 4px; -moz-border-radius:3px; border-radius:3px; -webkit-border-radius:3px; }
+ input[type='text']:focus { border: 2px solid #80B8D9; outline: 0 !important; }
+ input[type='checkbox'], input[type='radio'] { width: auto !important; float: left; margin-top: 3px; margin-right: 8px; }
+ input.fieldError, select.fieldError, textarea.fieldError { border: 2px solid #D56F55; }
+ img { border: none; }
+ label { color: #222; font-weight: bold; display: block; margin-bottom: 4px; }
+ label.radio { width: auto; margin: -2px 0 0 5px; }
+ label span.label-required { color: #AAA; margin-left: 4px; font-weight: normal; }
+ td { vertical-align: top; }
+ select { width: 300px; }
+ textarea { height: 100px; width: 311px; }
+ /* Core */
+ #media-upload-header { border-bottom: 1px solid #DFDFDF; font-weight:bold; margin:0; padding:3px 5px 0 5px; position:relative; background: #FFF; }
+ #sidemenu { bottom:-1px; font-size:12px; list-style:none outside none; padding-left:10px; position:relative; left:0; margin:0 5px; overflow:hidden; }
+ #sidemenu a { text-decoration:none; border-top: 1px solid #FFF; display:block; float:left; line-height:28px; padding:0 13px; outline: none; }
+ #sidemenu a.current { background-color:#F9F9F9; border-color:#DFDFDF #DFDFDF #F9F9F9; color:#D54E21; -moz-border-radius:4px 4px 0 0; border-radius:4px 4px 0 0; -webkit-border-radius:4px 4px 0 0; border-style:solid; border-width:1px; font-weight:normal; }
+ #sidemenu li { display:inline; margin-bottom:6px; line-height:200%; list-style:none outside none; margin:0; padding:0; text-align:center; white-space:nowrap; }
+ .button { background-color:#f2f2f2; border-color:#BBBBBB; min-width:80px; text-align:center; color:#464646; text-shadow:0 1px 0 #FFFFFF; border-style:solid; border-width:1px; cursor:pointer; width: auto; font-size:11px !important; line-height:13px; padding:3px 11px; margin-top: 12px; text-decoration:none; -moz-border-radius:11px; border-radius:11px; -webkit-border-radius:11px }
+ .button-primary { background-color:#21759B; font-weight: bold; border-color:#298CBA; text-align:center; color:#EAF2FA; text-shadow:0 -1px 0 rgba(0, 0, 0, 0.3); border-style:solid; border-width:1px; cursor:pointer; width: auto; font-size:11px !important; line-height:13px; padding:3px 11px; margin-top: 21px; text-decoration:none; -moz-border-radius:11px; border-radius:11px; -webkit-border-radius:11px }
+ .clear { clear: both; }
+ .fb-add-field { padding-left: 10px; }
+ .fb-add-option { margin: 0 0 14px 100px; }
+ .fb-container { margin: 21px; padding-bottom: 20px; }
+ .fb-desc, #fb-add-field { margin-top: 34px; }
+ .fb-extra-fields { margin-bottom: 2px; }
+ .fb-form-case { background: #FFF; padding: 13px; border: 1px solid #E2E2E2; width: 336px; -moz-border-radius:4px; border-radius:4px; -webkit-border-radius:4px }
+ .fb-form-case a { outline: none; }
+ .fb-form-case input[type='text'], .fb-form-case textarea { background: #E1E1E1; }
+ .fb-radio-label { display: inline-block; float: left; width: 290px; }
+ .fb-new-fields { position: relative; border: 1px dashed #FFF; background: #FFF; padding: 4px 10px 10px; cursor: default; }
+ .fb-new-fields:hover { border: 1px dashed #BBDBEA; background: #F7FBFD; }
+ .fb-options { width: 170px !important; }
+ .fb-remove { background: url('<?php echo GRUNION_PLUGIN_URL; ?>/images/grunion-remove-field.gif') no-repeat; position: absolute; cursor: pointer !important; right: -26px; top: 27px; width: 20px; height: 23px; }
+ .fb-remove:hover { background: url('<?php echo GRUNION_PLUGIN_URL; ?>/images/grunion-remove-field-hover.gif') no-repeat; }
+ .fb-remove-small { top: 2px !important; }
+ .fb-remove-option { position: absolute; top: 1px; right: 10px; width: 20px; height: 23px; background: url('<?php echo GRUNION_PLUGIN_URL; ?>/images/grunion-remove-option.gif') no-repeat; }
+ .fb-remove-option:hover { background: url('<?php echo GRUNION_PLUGIN_URL; ?>/images/grunion-remove-option-hover.gif') no-repeat; }
+ .fb-reorder { cursor: move; position: relative; }
+ .fb-reorder:hover div { display: block !important; width: 130px !important; position: absolute; top: 0; right: 0; z-index: 200; padding: 5px 10px; color: #555; font-size: 11px; background: #FFF; border: 1px solid #CCC; -moz-border-radius:4px; border-radius:4px; -webkit-border-radius:4px; }
+ .fb-right { position: absolute; right: 0; top: 0; width: 315px; margin: 57px 21px 0 0; }
+ .fb-right .fb-new-fields { border: none; background: #F9F9F9; padding: 0; }
+ .fb-right input[type='text'] { width: 195px; margin-bottom: 14px; }
+ .fb-right label { color: #444; width: 100px; float: left; font-weight: normal; }
+ .fb-right select { width: 195px !important; margin-bottom: 14px; }
+ .fb-right textarea { margin-bottom: 13px; }
+ .fb-right p { color: #999; line-height: 19px; }
+ .fb-settings input[type='text'], .fb-settings textarea { background-image: none !important; }
+ .fb-success { position: absolute; top: -3px; right: 100px; padding: 6px 23px 4px 23px; background: #FFFFE0; font-weight: normal; border: 1px solid #E6DB55; color: #333; -moz-border-radius:4px; border-radius:4px; -webkit-border-radius:4px; }
+ .right { float: right; }
+ /* rtl */
+ body.rtl{ direction: rtl; font-family:Tahoma,Arial,sans-serif}
+ .rtl input[type='text'] { margin-left: 4px; margin-right: 0; }
+ .rtl input[type='checkbox'], .rtl input[type='radio'] { float: right; }
+ .rtl input[type='radio'] { margin-left: 8px; margin-right: 0; }
+ .rtl label.radio { margin: -2px 5px 0 0; }
+ .rtl label span.label-required { margin-right: 4px; margin-left:0 }
+ .rtl #sidemenu { padding-right:10px; padding-left: 0; left:auto; right: 0; }
+ .rtl #sidemenu a { float:right; }
+ .rtl .fb-add-field { padding-right: 10px; padding-left: 0; }
+ .rtl .fb-add-option { margin: 0 100px 14px 0; }
+ .rtl .fb-radio-label { margin-right: 8px; margin-left: 0; float: right; }
+ .rtl .fb-remove { right: auto; left: -26px; transform: scaleX(-1); }
+ .rtl .fb-remove-option { right: auto; left: 10px; }
+ .rtl .fb-reorder:hover div { left: 0; right: auto; }
+ .rtl .fb-right { left: 0; right: auto; margin: 57px 0 0 21px; }
+ .rtl .fb-right label { float: right; }
+ .rtl .fb-success { right: auto; left: 100px;}
+ .rtl .right { float: left; }
+ @media only screen and (min--moz-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 1.5) {
+ .fb-remove { background: url('<?php echo GRUNION_PLUGIN_URL; ?>/images/grunion-remove-field-2x.png') no-repeat; background-size: 20px 23px; }
+ .fb-remove:hover { background: url('<?php echo GRUNION_PLUGIN_URL; ?>/images/grunion-remove-field-hover-2x.png') no-repeat; background-size: 20px 23px; }
+ .fb-remove-option { background: url('<?php echo GRUNION_PLUGIN_URL; ?>/images/grunion-remove-option-2x.png') no-repeat; background-size: 20px 23px; }
+ .fb-remove-option:hover { background: url('<?php echo GRUNION_PLUGIN_URL; ?>/images/grunion-remove-option-hover-2x.png') no-repeat; background-size: 20px 23px; }
+ }
+</style>
+</head>
+
+<body
+<?php
+if ( is_rtl() ) {
+ echo 'class="rtl"'; }
+?>
+>
+ <div id="media-upload-header">
+ <div id="fb-success" class="fb-success" style="display: none;"><?php esc_html_e( 'Your new field was saved successfully', 'jetpack' ); ?></div>
+ <ul id="sidemenu">
+ <li id="tab-preview"><a class="current" href=""><?php esc_html_e( 'Form builder', 'jetpack' ); ?></a></li>
+ <li id="tab-settings"><a href=""><?php esc_html_e( 'Email notifications', 'jetpack' ); ?></a></li>
+ </ul>
+ </div>
+ <div class="fb-right">
+ <div id="fb-desc" class="fb-desc">
+ <h3><?php esc_html_e( 'How does this work?', 'jetpack' ); ?></h3>
+ <p><?php esc_html_e( 'By adding a contact form, your readers will be able to submit feedback to you. All feedback is automatically scanned for spam, and the legitimate feedback will be emailed to you.', 'jetpack' ); ?></p>
+ <h3 style="margin-top: 21px;"><?php esc_html_e( 'Can I add more fields?', 'jetpack' ); ?></h3>
+ <p>
+ <?php
+ printf(
+ esc_html( _x( 'Sure thing. %1$s to add a new text box, textarea, radio, checkbox, or dropdown field.', '%1$s = "Click here" in an HTML link', 'jetpack' ) ),
+ '<a href="#" class="fb-add-field" style="padding-left: 0;">' . esc_html__( 'Click here', 'jetpack' ) . '</a>'
+ );
+ ?>
+ </p>
+ <h3 style="margin-top: 21px;"><?php esc_html_e( 'Can I view my feedback within WordPress?', 'jetpack' ); ?></h3>
+ <p>
+ <?php
+ printf(
+ esc_html( _x( 'Yep, you can read your feedback at any time by clicking the "%1$s" link in the admin menu.', '%1$s = "Feedback" in an HTML link', 'jetpack' ) ),
+ '<a id="fb-feedback" href="' . admin_url( 'edit.php?post_type=feedback' ) . '">' . esc_html__( 'Feedback', 'jetpack' ) . '</a>'
+ );
+ ?>
+ </p>
+ <div class="clear"></div>
+ </div>
+ <div id="fb-email-desc" class="fb-desc" style="display: none;">
+ <h3><?php esc_html_e( 'Do I need to fill this out?', 'jetpack' ); ?></h3>
+ <p><?php esc_html_e( 'Nope. However, if you&#8217;d like to modify where your feedback is sent, or the subject line you can. If you don&#8217;t make any changes here, feedback will be sent to the author of the page/post and the subject will be the name of this page/post.', 'jetpack' ); ?></p>
+ <h3 style="margin-top: 21px;"><?php esc_html_e( 'Can I send a notification to more than one person?', 'jetpack' ); ?></h3>
+ <p><?php esc_html_e( 'Yep. You can enter multiple email addresses in the Email address field, and separate them with commas. A notification email will then be sent to each email address.', 'jetpack' ); ?></p>
+ <div class="clear"></div>
+ </div>
+ <div id="fb-add-field" style="display: none;">
+ <h3><?php esc_html_e( 'Edit this new field', 'jetpack' ); ?></h3>
+
+ <label for="fb-new-label"><?php esc_html_e( 'Label', 'jetpack' ); ?></label>
+ <input type="text" id="fb-new-label" value="<?php esc_attr_e( 'New field', 'jetpack' ); ?>" />
+
+ <label for="fb-new-label"><?php esc_html_e( 'Field type', 'jetpack' ); ?></label>
+ <select id="fb-new-type">
+ <option value="checkbox"><?php esc_html_e( 'Checkbox', 'jetpack' ); ?></option>
+ <option value="checkbox-multiple"><?php esc_html_e( 'Checkbox with Multiple Items', 'jetpack' ); ?></option>
+ <option value="select"><?php esc_html_e( 'Drop down', 'jetpack' ); ?></option>
+ <option value="email"><?php esc_html_e( 'Email', 'jetpack' ); ?></option>
+ <option value="name"><?php esc_html_e( 'Name', 'jetpack' ); ?></option>
+ <option value="radio"><?php esc_html_e( 'Radio', 'jetpack' ); ?></option>
+ <option value="text" selected="selected"><?php esc_html_e( 'Text', 'jetpack' ); ?></option>
+ <option value="textarea"><?php esc_html_e( 'Textarea', 'jetpack' ); ?></option>
+ <option value="url"><?php esc_html_e( 'Website', 'jetpack' ); ?></option>
+ </select>
+ <div class="clear"></div>
+
+ <div id="fb-options" style="display: none;">
+ <div id="fb-new-options">
+ <label for="fb-option0"><?php esc_html_e( 'Options', 'jetpack' ); ?></label>
+ <input type="text" id="fb-option0" optionid="0" value="<?php esc_attr_e( 'First option', 'jetpack' ); ?>" class="fb-options" />
+ </div>
+ <div id="fb-add-option" class="fb-add-option">
+ <a href="#" id="fb-another-option"><?php esc_html_e( 'Add another option', 'jetpack' ); ?></a>
+ </div>
+ </div>
+
+ <div class="fb-required">
+ <label for="fb-new-label"></label>
+ <input type="checkbox" id="fb-new-required" />
+ <label for="fb-new-label" class="fb-radio-label"><?php esc_html_e( 'Required?', 'jetpack' ); ?></label>
+ <div class="clear"></div>
+ </div>
+
+ <input type="hidden" id="fb-field-id" />
+ <input type="submit" class="button" value="<?php esc_attr_e( 'Save this field', 'jetpack' ); ?>" id="fb-save-field" name="save">
+ </div>
+ </div>
+ <form id="fb-preview">
+ <div id="fb-preview-form" class="fb-container">
+ <h1><?php esc_html_e( 'Here&#8217;s what your form will look like', 'jetpack' ); ?></h1>
+ <div id="sortable" class="fb-form-case">
+
+ <div id="fb-extra-fields" class="fb-extra-fields"></div>
+
+ <a href="#" id="fb-new-field" class="fb-add-field"><?php esc_html_e( 'Add a new field', 'jetpack' ); ?></a>
+ </div>
+ <input type="submit" class="button-primary" tabindex="4" value="<?php esc_attr_e( 'Add this form to my post', 'jetpack' ); ?>" id="fb-save-form" name="save">
+ </div>
+ <div id="fb-email-settings" class="fb-container" style="display: none;">
+ <h1><?php esc_html_e( 'Email settings', 'jetpack' ); ?></h1>
+ <div class="fb-form-case fb-settings">
+ <label for="fb-fieldname"><?php esc_html_e( 'Enter your email address', 'jetpack' ); ?></label>
+ <input type="text" id="fb-field-my-email" style="background: #FFF !important;" />
+
+ <label for="fb-fieldemail" style="margin-top: 14px;"><?php esc_html_e( 'What should the subject line be?', 'jetpack' ); ?></label>
+ <input type="text" id="fb-field-subject" style="background: #FFF !important;" />
+ </div>
+ <input type="submit" class="button-primary" value="<?php esc_attr_e( 'Save and go back to form builder', 'jetpack' ); ?>" id="fb-prev-form" name="save">
+ </div>
+ </form>
+</body>
+</html>
diff --git a/plugins/jetpack/modules/contact-form/images/blank-screen-akismet.png b/plugins/jetpack/modules/contact-form/images/blank-screen-akismet.png
new file mode 100644
index 00000000..a4ba1e2d
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/blank-screen-akismet.png
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/blank-screen-button.png b/plugins/jetpack/modules/contact-form/images/blank-screen-button.png
new file mode 100644
index 00000000..58dfa26b
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/blank-screen-button.png
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/grunion-form-2x.png b/plugins/jetpack/modules/contact-form/images/grunion-form-2x.png
new file mode 100644
index 00000000..824d85a3
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/grunion-form-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/grunion-form.png b/plugins/jetpack/modules/contact-form/images/grunion-form.png
new file mode 100644
index 00000000..f4a0cc1d
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/grunion-form.png
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/grunion-remove-field-2x.png b/plugins/jetpack/modules/contact-form/images/grunion-remove-field-2x.png
new file mode 100644
index 00000000..bfbca5ed
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/grunion-remove-field-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/grunion-remove-field-hover-2x.png b/plugins/jetpack/modules/contact-form/images/grunion-remove-field-hover-2x.png
new file mode 100644
index 00000000..dca39da9
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/grunion-remove-field-hover-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/grunion-remove-field-hover.gif b/plugins/jetpack/modules/contact-form/images/grunion-remove-field-hover.gif
new file mode 100644
index 00000000..20d9e712
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/grunion-remove-field-hover.gif
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/grunion-remove-field.gif b/plugins/jetpack/modules/contact-form/images/grunion-remove-field.gif
new file mode 100644
index 00000000..55062664
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/grunion-remove-field.gif
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/grunion-remove-option-2x.png b/plugins/jetpack/modules/contact-form/images/grunion-remove-option-2x.png
new file mode 100644
index 00000000..4272442c
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/grunion-remove-option-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/grunion-remove-option-hover-2x.png b/plugins/jetpack/modules/contact-form/images/grunion-remove-option-hover-2x.png
new file mode 100644
index 00000000..210de1b3
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/grunion-remove-option-hover-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/grunion-remove-option-hover.gif b/plugins/jetpack/modules/contact-form/images/grunion-remove-option-hover.gif
new file mode 100644
index 00000000..9098b065
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/grunion-remove-option-hover.gif
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/images/grunion-remove-option.gif b/plugins/jetpack/modules/contact-form/images/grunion-remove-option.gif
new file mode 100644
index 00000000..ec491663
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/images/grunion-remove-option.gif
Binary files differ
diff --git a/plugins/jetpack/modules/contact-form/js/editor-view.js b/plugins/jetpack/modules/contact-form/js/editor-view.js
new file mode 100644
index 00000000..b62d92f8
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/js/editor-view.js
@@ -0,0 +1,284 @@
+/* global grunionEditorView, tinyMCE, QTags, wp */
+( function( $, wp, grunionEditorView ) {
+ wp.mce = wp.mce || {};
+ if ( 'undefined' === typeof wp.mce.views ) {
+ return;
+ }
+
+ wp.mce.grunion_wp_view_renderer = {
+ shortcode_string: 'contact-form',
+ template: wp.template( 'grunion-contact-form' ),
+ field_templates: {
+ email: wp.template( 'grunion-field-email' ),
+ telephone: wp.template( 'grunion-field-telephone' ),
+ textarea: wp.template( 'grunion-field-textarea' ),
+ radio: wp.template( 'grunion-field-radio' ),
+ checkbox: wp.template( 'grunion-field-checkbox' ),
+ 'checkbox-multiple': wp.template( 'grunion-field-checkbox-multiple' ),
+ select: wp.template( 'grunion-field-select' ),
+ date: wp.template( 'grunion-field-date' ),
+ text: wp.template( 'grunion-field-text' ),
+ name: wp.template( 'grunion-field-text' ),
+ url: wp.template( 'grunion-field-url' ),
+ },
+ edit_template: wp.template( 'grunion-field-edit' ),
+ editor_inline: wp.template( 'grunion-editor-inline' ),
+ editor_option: wp.template( 'grunion-field-edit-option' ),
+ getContent: function() {
+ var content = this.shortcode.content,
+ index = 0,
+ field,
+ named,
+ body = '';
+
+ // If it's the legacy `[contact-form /]` syntax, populate default fields.
+ if ( ! content ) {
+ content = grunionEditorView.default_form;
+ }
+
+ // Render the fields.
+ while ( ( field = wp.shortcode.next( 'contact-field', content, index ) ) ) {
+ index = field.index + field.content.length;
+ named = field.shortcode.attrs.named;
+ if ( ! named.type || ! this.field_templates[ named.type ] ) {
+ named.type = 'text';
+ }
+ if ( named.required ) {
+ named.required = grunionEditorView.labels.required_field_text;
+ }
+ if ( named.options && 'string' === typeof named.options ) {
+ named.options = named.options.split( ',' );
+ }
+ body += this.field_templates[ named.type ]( named );
+ }
+
+ var options = {
+ body: body,
+ submit_button_text: grunionEditorView.labels.submit_button_text,
+ };
+
+ return this.template( options );
+ },
+ edit: function( data, update_callback ) {
+ var shortcode_data = wp.shortcode.next( this.shortcode_string, data ),
+ shortcode = shortcode_data.shortcode,
+ $tinyMCE_document = $( tinyMCE.activeEditor.getDoc() ),
+ $view = $tinyMCE_document.find( '.wpview.wpview-wrap' ).filter( function() {
+ return $( this ).attr( 'data-mce-selected' );
+ } ),
+ $editframe = $( '<iframe scrolling="no" class="inline-edit-contact-form" />' ),
+ index = 0,
+ named,
+ fields = '',
+ field;
+
+ if ( ! shortcode.content ) {
+ shortcode.content = grunionEditorView.default_form;
+ }
+
+ // Render the fields.
+ while ( ( field = wp.shortcode.next( 'contact-field', shortcode.content, index ) ) ) {
+ index = field.index + field.content.length;
+ named = field.shortcode.attrs.named;
+ if ( named.options && 'string' === typeof named.options ) {
+ named.options = named.options.split( ',' );
+ }
+ fields += this.edit_template( named );
+ }
+
+ $editframe.on( 'checkheight', function() {
+ var innerDoc = this.contentDocument ? this.contentDocument : this.contentWindow.document;
+ this.style.height = '10px';
+ this.style.height = 5 + innerDoc.body.scrollHeight + 'px';
+ tinyMCE.activeEditor.execCommand( 'wpAutoResize' );
+ } );
+
+ $editframe.on( 'load', function() {
+ var stylesheet_url =
+ 1 === window.isRtl
+ ? grunionEditorView.inline_editing_style_rtl
+ : grunionEditorView.inline_editing_style,
+ $stylesheet = $( '<link rel="stylesheet" href="' + stylesheet_url + '" />' ),
+ $dashicons_css = $(
+ '<link rel="stylesheet" href="' + grunionEditorView.dashicons_css_url + '" />'
+ );
+
+ $stylesheet.on( 'load', function() {
+ $editframe
+ .contents()
+ .find( 'body' )
+ .css( 'visibility', 'visible' );
+ $editframe.trigger( 'checkheight' );
+ } );
+ $editframe
+ .contents()
+ .find( 'head' )
+ .append( $stylesheet )
+ .append( $dashicons_css );
+
+ $editframe
+ .contents()
+ .find( 'body' )
+ .html(
+ wp.mce.grunion_wp_view_renderer.editor_inline( {
+ to: shortcode.attrs.named.to,
+ subject: shortcode.attrs.named.subject,
+ fields: fields,
+ } )
+ )
+ .css( 'visibility', 'hidden' );
+
+ $editframe
+ .contents()
+ .find( 'input:first' )
+ .focus();
+
+ setTimeout( function() {
+ $editframe.trigger( 'checkheight' );
+ }, 250 );
+
+ // Add a second timeout for super long forms racing, and to not slow it down for shorter forms unnecessarily.
+ setTimeout( function() {
+ $editframe.trigger( 'checkheight' );
+ }, 500 );
+
+ var $editfields = $editframe.contents().find( '.grunion-fields' ),
+ $buttons = $editframe.contents().find( '.grunion-controls' );
+
+ $editfields.sortable();
+
+ // Now, add all the listeners!
+
+ $editfields.on( 'change select', 'select[name=type]', function() {
+ $( this ).closest( '.grunion-field-edit' )[ 0 ].className =
+ 'card is-compact grunion-field-edit grunion-field-' + $( this ).val();
+ $editframe.trigger( 'checkheight' );
+ } );
+
+ $editfields.on( 'click', '.delete-option', function( e ) {
+ e.preventDefault();
+ $( this )
+ .closest( 'li' )
+ .remove();
+ $editframe.trigger( 'checkheight' );
+ } );
+
+ $editfields.on( 'click', '.add-option', function( e ) {
+ var $new_option = $( wp.mce.grunion_wp_view_renderer.editor_option() );
+ e.preventDefault();
+ $( this )
+ .closest( 'li' )
+ .before( $new_option );
+ $editframe.trigger( 'checkheight' );
+ $new_option.find( 'input:first' ).focus();
+ } );
+
+ $editfields.on( 'click', '.delete-field', function( e ) {
+ e.preventDefault();
+ $( this )
+ .closest( '.card' )
+ .remove();
+ $editframe.trigger( 'checkheight' );
+ } );
+
+ $buttons.find( 'input[name=submit]' ).on( 'click', function() {
+ var new_data = shortcode;
+
+ new_data.type = 'closed';
+ new_data.attrs = {};
+ new_data.content = '';
+
+ $editfields.children().each( function() {
+ var field_shortcode = {
+ tag: 'contact-field',
+ type: 'single',
+ attrs: {
+ label: $( this )
+ .find( 'input[name=label]' )
+ .val(),
+ type: $( this )
+ .find( 'select[name=type]' )
+ .val(),
+ },
+ },
+ options = [];
+
+ if ( $( this ).find( 'input[name=required]:checked' ).length ) {
+ field_shortcode.attrs.required = '1';
+ }
+
+ $( this )
+ .find( 'input[name=option]' )
+ .each( function() {
+ if ( $( this ).val() ) {
+ options.push( $( this ).val() );
+ }
+ } );
+ if ( options.length ) {
+ field_shortcode.attrs.options = options.join( ',' );
+ }
+
+ new_data.content += wp.shortcode.string( field_shortcode );
+ } );
+
+ if (
+ $editframe
+ .contents()
+ .find( 'input[name=to]' )
+ .val()
+ ) {
+ new_data.attrs.to = $editframe
+ .contents()
+ .find( 'input[name=to]' )
+ .val();
+ }
+ if (
+ $editframe
+ .contents()
+ .find( 'input[name=subject]' )
+ .val()
+ ) {
+ new_data.attrs.subject = $editframe
+ .contents()
+ .find( 'input[name=subject]' )
+ .val();
+ }
+
+ update_callback( wp.shortcode.string( new_data ) );
+ } );
+
+ $buttons.find( 'input[name=cancel]' ).on( 'click', function() {
+ update_callback( wp.shortcode.string( shortcode ) );
+ } );
+
+ $buttons.find( 'input[name=add-field]' ).on( 'click', function() {
+ var $new_field = $( wp.mce.grunion_wp_view_renderer.edit_template( {} ) );
+ $editfields.append( $new_field );
+ $editfields.sortable( 'refresh' );
+ $editframe.trigger( 'checkheight' );
+ $new_field.find( 'input:first' ).focus();
+ } );
+ } );
+
+ $view.html( $editframe );
+ },
+ };
+ wp.mce.views.register( 'contact-form', wp.mce.grunion_wp_view_renderer );
+
+ // Add the 'text' editor button.
+ QTags.addButton( 'grunion_shortcode', grunionEditorView.labels.quicktags_label, function() {
+ QTags.insertContent( '[contact-form]' + grunionEditorView.default_form + '[/contact-form]' );
+ } );
+
+ var $wp_content_wrap = $( '#wp-content-wrap' );
+ $( '#insert-jetpack-contact-form' ).on( 'click', function( e ) {
+ e.preventDefault();
+ if ( $wp_content_wrap.hasClass( 'tmce-active' ) ) {
+ tinyMCE.execCommand( 'grunion_add_form' );
+ } else if ( $wp_content_wrap.hasClass( 'html-active' ) ) {
+ QTags.insertContent( '[contact-form]' + grunionEditorView.default_form + '[/contact-form]' );
+ } else {
+ window.console.error( 'Neither TinyMCE nor QuickTags is active. Unable to insert form.' );
+ }
+ } );
+} )( jQuery, wp, grunionEditorView );
diff --git a/plugins/jetpack/modules/contact-form/js/grunion-admin.js b/plugins/jetpack/modules/contact-form/js/grunion-admin.js
new file mode 100644
index 00000000..100fb22b
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/js/grunion-admin.js
@@ -0,0 +1,30 @@
+/* global ajaxurl */
+jQuery( function( $ ) {
+ $( document ).on( 'click', '#jetpack-check-feedback-spam:not(.button-disabled)', function( e ) {
+ e.preventDefault();
+
+ $( '#jetpack-check-feedback-spam:not(.button-disabled)' ).addClass( 'button-disabled' );
+ $( '.jetpack-check-feedback-spam-spinner' )
+ .addClass( 'spinner' )
+ .show();
+ grunion_check_for_spam( 0, 100 );
+ } );
+
+ function grunion_check_for_spam( offset, limit ) {
+ $.post(
+ ajaxurl,
+ {
+ action: 'grunion_recheck_queue',
+ offset: offset,
+ limit: limit,
+ },
+ function( result ) {
+ if ( result.processed < limit ) {
+ window.location.reload();
+ } else {
+ grunion_check_for_spam( offset + limit, limit );
+ }
+ }
+ );
+ }
+} );
diff --git a/plugins/jetpack/modules/contact-form/js/grunion-frontend.js b/plugins/jetpack/modules/contact-form/js/grunion-frontend.js
new file mode 100644
index 00000000..1c1819a5
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/js/grunion-frontend.js
@@ -0,0 +1,3 @@
+jQuery( function( $ ) {
+ $( '.contact-form input.jp-contact-form-date' ).datepicker();
+} );
diff --git a/plugins/jetpack/modules/contact-form/js/grunion.js b/plugins/jetpack/modules/contact-form/js/grunion.js
new file mode 100644
index 00000000..e34fc135
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/js/grunion.js
@@ -0,0 +1,1180 @@
+/* jshint onevar: false, devel: true, smarttabs: true */
+/* global GrunionFB_i18n: true, FB, ajax_nonce_shortcode, ajax_nonce_json, ajaxurl, postId */
+
+if ( ! window.FB ) {
+ window.FB = {};
+}
+
+GrunionFB_i18n = jQuery.extend(
+ {
+ nameLabel: 'Name',
+ emailLabel: 'Email',
+ urlLabel: 'Website',
+ commentLabel: 'Comment',
+ newLabel: 'New Field',
+ optionsLabel: 'Options',
+ optionLabel: 'Option',
+ firstOptionLabel: 'First option',
+ problemGeneratingForm:
+ "Oops, there was a problem generating your form. You'll likely need to try again.",
+ moveInstructions: 'Drag up or down\nto re-arrange',
+ moveLabel: 'move',
+ editLabel: 'edit',
+ savedMessage: 'Saved successfully',
+ requiredLabel: '(required)',
+ exitConfirmMessage:
+ 'Are you sure you want to exit the form editor without saving? Any changes you have made will be lost.',
+ maxNewFields: 5,
+ invalidEmail: ' is an invalid email address.',
+ },
+ GrunionFB_i18n
+);
+
+GrunionFB_i18n.moveInstructions = GrunionFB_i18n.moveInstructions.replace( '\n', '<br />' );
+
+FB.span = jQuery( '<span>' );
+FB.esc_html = function( string ) {
+ return FB.span.text( string ).html();
+};
+
+FB.esc_attr = function( string ) {
+ string = FB.esc_html( string );
+ return string.replace( '"', '&quot;' ).replace( "'", '&#039;' );
+};
+
+FB.ContactForm = ( function() {
+ var fbForm = {
+ // Main object that generated shortcode via AJAX call
+ action: 'grunion_shortcode',
+ _ajax_nonce: ajax_nonce_shortcode,
+ to: '',
+ subject: '',
+ fields: {},
+ };
+ var defaultFields = {
+ name: {
+ label: GrunionFB_i18n.nameLabel,
+ type: 'name',
+ required: true,
+ options: [],
+ order: '1',
+ },
+ email: {
+ label: GrunionFB_i18n.emailLabel,
+ type: 'email',
+ required: true,
+ options: [],
+ order: '2',
+ },
+ url: {
+ label: GrunionFB_i18n.urlLabel,
+ type: 'url',
+ required: false,
+ options: [],
+ order: '3',
+ },
+ comment: {
+ label: GrunionFB_i18n.commentLabel,
+ type: 'textarea',
+ required: true,
+ options: [],
+ order: '4',
+ },
+ };
+ var debug = false; // will print errors to log if true
+ var grunionNewCount = 0; // increment for new fields
+ var maxNewFields = GrunionFB_i18n.maxNewFields; // See filter in ../grunion-form-view.php
+ var optionsCache = {};
+ var optionsCount = 0; // increment for options
+ var shortcode;
+
+ function addField() {
+ try {
+ grunionNewCount++;
+ if ( grunionNewCount <= maxNewFields ) {
+ // Add to preview
+ jQuery( '#fb-extra-fields' ).append(
+ '<div id="fb-new-field' +
+ grunionNewCount +
+ '" fieldid="' +
+ grunionNewCount +
+ '" class="fb-new-fields"><div class="fb-fields"><div id="' +
+ grunionNewCount +
+ '" class="fb-remove"></div><label fieldid="' +
+ grunionNewCount +
+ '" for="fb-field' +
+ grunionNewCount +
+ '"><span class="label-text">' +
+ GrunionFB_i18n.newLabel +
+ '</span> </label><input type="text" id="fb-field' +
+ grunionNewCount +
+ '" disabled="disabled" /></div></div>'
+ );
+ // Add to form object
+ fbForm.fields[ grunionNewCount ] = {
+ label: GrunionFB_i18n.newLabel,
+ type: 'text',
+ required: false,
+ options: [],
+ order: '5',
+ };
+ if ( grunionNewCount === maxNewFields ) {
+ jQuery( '#fb-new-field' ).hide();
+ }
+ // Reset form for this new field
+ optionsCount = 0;
+ optionsCache = {};
+ jQuery( '#fb-new-options' ).html(
+ '<label for="fb-option0">' +
+ GrunionFB_i18n.optionsLabel +
+ '</label><input type="text" id="fb-option0" optionid="0" value="' +
+ GrunionFB_i18n.firstOptionLabel +
+ '" class="fb-options" />'
+ );
+ jQuery( '#fb-options' ).hide();
+ jQuery( '#fb-new-label' ).val( GrunionFB_i18n.newLabel );
+ jQuery( '#fb-new-type' ).val( 'text' );
+ jQuery( '#fb-field-id' ).val( grunionNewCount );
+ setTimeout( function() {
+ jQuery( '#fb-new-label' )
+ .focus()
+ .select();
+ }, 100 );
+ } else {
+ jQuery( '#fb-new-field' ).hide();
+ }
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'addField(): ' + e );
+ }
+ }
+ }
+ function addOption() {
+ try {
+ optionsCount = jQuery( '#fb-new-options .fb-options' ).length;
+ var thisId = jQuery( '#fb-field-id' ).val();
+ var thisType = jQuery( '#fb-new-type' ).val();
+ if ( thisType === 'radio' ) {
+ // Add to right col
+ jQuery( '#fb-new-options' ).append(
+ '<div id="fb-option-box-' +
+ optionsCount +
+ '" class="fb-new-fields"><span optionid="' +
+ optionsCount +
+ '" class="fb-remove-option"></span><label></label><input type="text" id="fb-option' +
+ optionsCount +
+ '" optionid="' +
+ optionsCount +
+ '" value="' +
+ GrunionFB_i18n.optionLabel +
+ '" class="fb-options" /><div>'
+ );
+ // Add to preview
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).append(
+ '<div id="fb-radio-' +
+ thisId +
+ '-' +
+ optionsCount +
+ '"><input type="radio" disabled="disabled" id="fb-field' +
+ thisId +
+ '" name="radio-' +
+ thisId +
+ '" /><span>' +
+ GrunionFB_i18n.optionLabel +
+ '</span><div class="clear"></div></div>'
+ );
+ } else if ( 'checkbox-multiple' === thisType ) {
+ // Add to right col
+ jQuery( '#fb-new-options' ).append(
+ '<div id="fb-option-box-' +
+ optionsCount +
+ '" class="fb-new-fields"><span optionid="' +
+ optionsCount +
+ '" class="fb-remove-option"></span><label></label><input type="text" id="fb-option' +
+ optionsCount +
+ '" optionid="' +
+ optionsCount +
+ '" value="' +
+ GrunionFB_i18n.optionLabel +
+ '" class="fb-options" /><div>'
+ );
+ // Add to preview
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).append(
+ '<div id="fb-checkbox-multiple-' +
+ thisId +
+ '-' +
+ optionsCount +
+ '"><input type="checkbox" disabled="disabled" id="fb-field' +
+ thisId +
+ '" name="checkbox-multiple-' +
+ thisId +
+ '" /><span>' +
+ GrunionFB_i18n.optionLabel +
+ '</span><div class="clear"></div></div>'
+ );
+ } else {
+ // Add to right col
+ jQuery( '#fb-new-options' ).append(
+ '<div id="fb-option-box-' +
+ optionsCount +
+ '" class="fb-new-fields"><span optionid="' +
+ optionsCount +
+ '" class="fb-remove-option"></span><label></label><input type="text" id="fb-option' +
+ optionsCount +
+ '" optionid="' +
+ optionsCount +
+ '" value="" class="fb-options" /><div>'
+ );
+ // Add to preview
+ jQuery( '#fb-field' + thisId ).append(
+ '<option id="fb-' +
+ thisId +
+ '-' +
+ optionsCount +
+ '" value="' +
+ thisId +
+ '-' +
+ optionsCount +
+ '"></option>'
+ );
+ }
+ // Add to fbForm object
+ fbForm.fields[ thisId ].options[ optionsCount ] = '';
+ // Add focus to new field
+ jQuery( '#fb-option' + optionsCount )
+ .focus()
+ .select();
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'addOption(): ' + e );
+ }
+ }
+ }
+ function buildPreview() {
+ try {
+ if ( fbForm.to ) {
+ jQuery( '#fb-field-my-email' ).val( fbForm.to );
+ }
+ if ( fbForm.subject ) {
+ jQuery( '#fb-field-subject' ).val( fbForm.subject );
+ }
+ // Loop over and add fields
+ jQuery.each( fbForm.fields, function( index, value ) {
+ jQuery( '#fb-extra-fields' ).before(
+ '<div class="fb-new-fields ui-state-default" fieldid="' +
+ index +
+ '" id="fb-new-field' +
+ index +
+ '"><div class="fb-fields"></div></div>'
+ );
+ jQuery( '#fb-field-id' ).val( index );
+ optionsCache[ index ] = {};
+ optionsCache[ index ].options = [];
+ if (
+ 'radio' === value.type ||
+ 'select' === value.type ||
+ 'checkbox-multiple' === value.type
+ ) {
+ jQuery.each( value.options, function( i, value ) {
+ optionsCache[ index ].options[ i ] = value;
+ } );
+ }
+ updateType( value.type, value.label, value.required );
+ } );
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'buildPreview(): ' + e );
+ }
+ }
+ }
+ function customOptions( id, thisType ) {
+ try {
+ var thisOptions = '';
+ for ( var i = 0; i < optionsCache[ id ].options.length; i++ ) {
+ if ( optionsCache[ id ].options[ i ] !== undefined ) {
+ if ( thisType === 'radio' ) {
+ thisOptions =
+ thisOptions +
+ '<div id="fb-radio-' +
+ id +
+ '-' +
+ i +
+ '"><input type="radio" id="fb-field' +
+ id +
+ '" name="radio-' +
+ id +
+ '" /><span>' +
+ FB.esc_html( optionsCache[ id ].options[ i ] ) +
+ '</span><div class="clear"></div></div>';
+ } else if ( 'checkbox-multiple' === thisType ) {
+ thisOptions =
+ thisOptions +
+ '<div id="fb-checkbox-multiple-' +
+ id +
+ '-' +
+ i +
+ '"><input type="checkbox" id="fb-field' +
+ id +
+ '" name="checkbox-multiple-' +
+ id +
+ '" /><span>' +
+ FB.esc_html( optionsCache[ id ].options[ i ] ) +
+ '</span><div class="clear"></div></div>';
+ } else {
+ thisOptions =
+ thisOptions +
+ '<option id="fb-' +
+ id +
+ '-' +
+ i +
+ '" value="' +
+ id +
+ '-' +
+ i +
+ '">' +
+ FB.esc_html( optionsCache[ id ].options[ i ] ) +
+ '</option>';
+ }
+ }
+ }
+ return thisOptions;
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'customOptions(): ' + e );
+ }
+ }
+ }
+ function deleteField( that ) {
+ try {
+ grunionNewCount--;
+ var thisId = that.attr( 'id' );
+ delete fbForm.fields[ thisId ];
+ jQuery( '#' + thisId )
+ .parent()
+ .parent()
+ .remove();
+ if ( grunionNewCount <= maxNewFields ) {
+ jQuery( '#fb-new-field' ).show();
+ }
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'deleteField(): ' + e );
+ }
+ }
+ }
+ function editField( that ) {
+ try {
+ scroll( 0, 0 );
+ setTimeout( function() {
+ jQuery( '#fb-new-label' )
+ .focus()
+ .select();
+ }, 100 );
+ var thisId = that.parent().attr( 'fieldid' );
+ loadFieldEditor( thisId );
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'editField(): ' + e );
+ }
+ }
+ }
+ function grabShortcode() {
+ try {
+ // Takes fbForm object and returns shortcode syntax
+ jQuery.post( ajaxurl, fbForm, function( response ) {
+ shortcode = response;
+ } );
+ } catch ( e ) {
+ alert( GrunionFB_i18n.problemGeneratingForm );
+ if ( debug ) {
+ console.log( 'grabShortcode(): ' + e );
+ }
+ }
+ }
+ function hideDesc() {
+ jQuery( '#fb-desc' ).hide();
+ jQuery( '#fb-add-field' ).show();
+ }
+ function hidePopup() {
+ try {
+ // copied from wp-includes/js/thickbox/thickbox.js
+ jQuery( '#TB_imageOff', window.parent.document ).unbind( 'click' );
+ jQuery( '#TB_closeWindowButton', window.parent.document ).unbind( 'click' );
+ jQuery( '#TB_window', window.parent.document ).fadeOut( 'fast' );
+ jQuery( 'body', window.parent.document ).removeClass( 'modal-open' );
+ jQuery( '#TB_window,#TB_overlay,#TB_HideSelect', window.parent.document )
+ .trigger( 'unload' )
+ .unbind()
+ .remove();
+ jQuery( '#TB_load', window.parent.document ).remove();
+ if ( typeof window.parent.document.body.style.maxHeight === 'undefined' ) {
+ //if IE 6
+ jQuery( 'body', 'html', window.parent.document ).css( { height: 'auto', width: 'auto' } );
+ jQuery( 'html', window.parent.document ).css( 'overflow', '' );
+ }
+ window.parent.document.onkeydown = '';
+ window.parent.document.onkeyup = '';
+ return false;
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'hidePopup(): ' + e );
+ }
+ }
+ }
+ function hideShowEditLink( whichType, that ) {
+ try {
+ if ( whichType === 'show' ) {
+ // Prevents showing links twice
+ if ( jQuery( '.fb-edit-field' ).is( ':visible' ) ) {
+ jQuery( '.fb-edit-field' ).remove();
+ }
+ that
+ .find( 'label' )
+ .prepend(
+ '<span class="right fb-edit-field" style="font-weight: normal;"><a href="" class="fb-reorder"><div style="display: none;">' +
+ GrunionFB_i18n.moveInstructions +
+ '</div>' +
+ GrunionFB_i18n.moveLabel +
+ '</a>&nbsp;&nbsp;<span style="color: #C7D8DE;">|</span>&nbsp;&nbsp;<a href="" class="fb-edit">' +
+ GrunionFB_i18n.editLabel +
+ '</a></span>'
+ );
+ } else {
+ jQuery( '.fb-edit-field' ).remove();
+ }
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'hideShowEditLink(): ' + e );
+ }
+ }
+ }
+ function loadFieldEditor( id ) {
+ try {
+ var thisType = fbForm.fields[ id ].type;
+ jQuery( '#fb-options' ).hide();
+ // Reset hidden field ID
+ jQuery( '#fb-field-id' ).val( id );
+ // Load label
+ jQuery( '#fb-new-label' ).val( fbForm.fields[ id ].label );
+ // Load type
+ jQuery( '#fb-new-type' ).val( fbForm.fields[ id ].type );
+ // Load required
+ if ( fbForm.fields[ id ].required ) {
+ jQuery( '#fb-new-required' ).prop( 'checked', true );
+ } else {
+ jQuery( '#fb-new-required' ).prop( 'checked', false );
+ }
+ // Load options if there are any
+ if ( 'select' === thisType || 'radio' === thisType || 'checkbox-multiple' === thisType ) {
+ var thisOptions = fbForm.fields[ id ].options;
+ jQuery( '#fb-options' ).show();
+ jQuery( '#fb-new-options' ).html( '' ); // Clear it all out
+ for ( var i = 0; i < thisOptions.length; i++ ) {
+ if ( thisOptions[ i ] !== undefined ) {
+ if ( thisType === 'radio' ) {
+ jQuery( '#fb-new-options' ).append(
+ '<div id="fb-option-box-' +
+ i +
+ '" class="fb-new-fields"><span optionid="' +
+ i +
+ '" class="fb-remove-option"></span><label></label><input type="text" id="fb-option' +
+ i +
+ '" optionid="' +
+ i +
+ '" value="' +
+ FB.esc_attr( fbForm.fields[ id ].options[ i ] ) +
+ '" class="fb-options" /><div>'
+ );
+ } else {
+ jQuery( '#fb-new-options' ).append(
+ '<div id="fb-option-box-' +
+ i +
+ '" class="fb-new-fields"><span optionid="' +
+ i +
+ '" class="fb-remove-option"></span><label></label><input type="text" id="fb-option' +
+ i +
+ '" optionid="' +
+ i +
+ '" value="' +
+ FB.esc_attr( fbForm.fields[ id ].options[ i ] ) +
+ '" class="fb-options" /><div>'
+ );
+ }
+ }
+ }
+ }
+ // Load editor & hide description
+ hideDesc();
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'loadFieldEditor(): ' + e );
+ }
+ }
+ }
+ function parseShortcode( data ) {
+ try {
+ // Clean up fields by resetting them
+ fbForm.fields = {};
+ // Add new fields
+ if ( ! data ) {
+ fbForm.fields = defaultFields;
+ } else {
+ jQuery.each( data.fields, function( index, value ) {
+ if ( 1 === parseInt( value.required, 10 ) ) {
+ value.required = 'true';
+ }
+ fbForm.fields[ index ] = value;
+ } );
+ fbForm.to = data.to;
+ fbForm.subject = data.subject;
+ }
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'parseShortcode(): ' + e );
+ }
+ }
+ }
+ function removeOption( optionId ) {
+ try {
+ var thisId = jQuery( '#fb-field-id' ).val();
+ var thisVal = jQuery( '#fb-option' + optionId ).val();
+ var thisType = jQuery( '#fb-new-type' ).val();
+ // Remove from right
+ jQuery( '#fb-option-box-' + optionId ).remove();
+ // Remove from preview
+ if ( thisType === 'radio' ) {
+ jQuery( '#fb-radio-' + thisId + '-' + optionId ).remove();
+ } else if ( 'checkbox-multiple' === thisType ) {
+ jQuery( '#fb-checkbox-multiple-' + thisId + '-' + optionId ).remove();
+ } else {
+ jQuery( '#fb-' + thisId + '-' + optionId ).remove();
+ }
+ // Remove from fbForm object
+ var idx = fbForm.fields[ thisId ].options.indexOf( thisVal );
+ if ( idx !== -1 ) {
+ fbForm.fields[ thisId ].options.splice( idx, 1 );
+ }
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'removeOption(): ' + e );
+ }
+ }
+ }
+ function removeOptions() {
+ try {
+ var thisId = jQuery( '#fb-field-id' ).val();
+ jQuery( '#fb-options' ).hide();
+ if ( optionsCache[ thisId ] === undefined ) {
+ optionsCache[ thisId ] = {};
+ }
+ optionsCache[ thisId ].options = fbForm.fields[ thisId ].options; // Save options in case they change their mind
+ fbForm.fields[ thisId ].options = []; // Removes all options
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'removeOptions(): ' + e );
+ }
+ }
+ }
+ function sendShortcodeToEditor() {
+ try {
+ // Serialize fields
+ jQuery( 'div#sortable div.fb-new-fields' ).each( function( index ) {
+ var thisId = jQuery( this ).attr( 'fieldid' );
+ fbForm.fields[ thisId ].order = index;
+ } );
+ // Export to WYSIWYG editor
+ jQuery.post( ajaxurl, fbForm, function( response ) {
+ var isVisual = jQuery( '#edButtonPreview', window.parent.document ).hasClass( 'active' );
+ /* WP 3.3+ */
+ if ( ! isVisual ) {
+ isVisual = jQuery( '#wp-content-wrap', window.parent.document ).hasClass( 'tmce-active' );
+ }
+
+ var win = window.dialogArguments || opener || parent || top;
+ var currentCode;
+ if ( isVisual ) {
+ currentCode = win.tinyMCE.activeEditor.getContent();
+ } else {
+ currentCode = jQuery( '#editorcontainer textarea', window.parent.document ).val();
+ /* WP 3.3+ */
+ if ( typeof currentCode !== 'string' ) {
+ currentCode = jQuery( '.wp-editor-area', window.parent.document ).val();
+ }
+ }
+ var regexp = new RegExp(
+ '\\[contact-form\\b.*?\\/?\\](?:[\\s\\S]+?\\[\\/contact-form\\])?'
+ );
+
+ // Remove new lines that cause BR tags to show up
+ response = response.replace( /\n/g, ' ' );
+ // Convert characters to comma
+ response = response.replace( /%26#x002c;/g, ',' );
+
+ // Add new shortcode
+ if ( currentCode.match( regexp ) ) {
+ if ( isVisual ) {
+ win.tinyMCE.activeEditor.execCommand(
+ 'mceSetContent',
+ false,
+ currentCode.replace( regexp, response )
+ );
+ } else {
+ // looks like the visual editor is disabled,
+ // update the contents of the post directly
+ jQuery( '#content', window.parent.document ).val(
+ currentCode.replace( regexp, response )
+ );
+ }
+ } else {
+ try {
+ win.send_to_editor( response );
+ } catch ( e ) {
+ if ( isVisual ) {
+ win.tinyMCE.activeEditor.execCommand( 'mceInsertContent', false, response );
+ } else {
+ // looks like the visual editor is disabled,
+ // update the contents of the post directly
+ jQuery( '#content', window.parent.document ).val( currentCode + response );
+ }
+ }
+ }
+ hidePopup();
+ } );
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'sendShortcodeToEditor(): ' + e );
+ }
+ }
+ }
+ function showDesc() {
+ jQuery( '#fb-desc' ).show();
+ jQuery( '#fb-add-field' ).hide();
+ }
+ function showAndHideMessage( message ) {
+ try {
+ var newMessage = ! message ? GrunionFB_i18n.savedMessage : message;
+ jQuery( '#fb-success' ).text( newMessage );
+ jQuery( '#fb-success' ).slideDown( 'fast' );
+ setTimeout( function() {
+ jQuery( '#fb-success' ).slideUp( 'fast' );
+ }, 2500 );
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'showAndHideMessage(): ' + e );
+ }
+ }
+ }
+ function switchTabs( whichType ) {
+ try {
+ if ( whichType === 'preview' ) {
+ if ( ! validateEmails( jQuery( '#fb-field-my-email' ).val() ) ) {
+ return;
+ }
+ jQuery( '#tab-preview a' ).addClass( 'current' );
+ jQuery( '#tab-settings a' ).removeClass( 'current' );
+ jQuery( '#fb-preview-form, #fb-desc' ).show();
+ jQuery( '#fb-email-settings, #fb-email-desc' ).hide();
+ showAndHideMessage( GrunionFB_i18n.savedMessage );
+ } else {
+ jQuery( '#tab-preview a' ).removeClass( 'current' );
+ jQuery( '#tab-settings a' ).addClass( 'current' );
+ jQuery( '#fb-preview-form, #fb-desc, #fb-add-field' ).hide();
+ jQuery( '#fb-email-settings, #fb-email-desc' ).show();
+ jQuery( '#fb-field-my-email' )
+ .focus()
+ .select();
+ }
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'switchTabs(): ' + e );
+ }
+ }
+ }
+ function validateEmails( emails ) {
+ // Field is allowed to be empty :)
+ if ( 0 === emails.length ) {
+ return true;
+ }
+
+ var $e,
+ emailList = emails.split( ',' );
+
+ for ( $e = 0; $e < emailList.length; $e++ ) {
+ if ( false === validateEmail( emailList[ $e ] ) ) {
+ alert( emailList[ $e ] + GrunionFB_i18n.invalidEmail );
+ return false;
+ }
+ }
+
+ return true;
+ }
+ /* Uses The Official Standard: RFC 5322 -- http://www.regular-expressions.info/email.html */
+ function validateEmail( email ) {
+ var re = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i;
+ return re.test( email );
+ }
+ function updateLabel() {
+ try {
+ var thisId = jQuery( '#fb-field-id' ).val();
+ var thisLabel = jQuery( '#fb-new-label' ).val();
+ // Update preview
+ if ( thisLabel.length === 0 ) {
+ jQuery( '#fb-new-field' + thisId + ' label .label-text' ).text( GrunionFB_i18n.newLabel );
+ } else {
+ jQuery( '#fb-new-field' + thisId + ' label .label-text' ).text( thisLabel );
+ }
+ // Update fbForm object
+ fbForm.fields[ thisId ].label = thisLabel;
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'updateLabel(): ' + e );
+ }
+ }
+ }
+ function updateMyEmail() {
+ try {
+ var thisEmail = jQuery( '#fb-field-my-email' ).val();
+ fbForm.to = thisEmail;
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'updateMyEmail(): ' + e );
+ }
+ }
+ }
+ function updateOption( that ) {
+ try {
+ var thisId = jQuery( '#fb-field-id' ).val();
+ var thisOptionid = that.attr( 'optionid' );
+ var thisOptionValue = that.val();
+ var thisType = jQuery( '#fb-new-type' ).val();
+ // Update preview
+ if ( thisType === 'radio' ) {
+ jQuery( '#fb-radio-' + thisId + '-' + thisOptionid + ' span' ).text( thisOptionValue );
+ } else if ( 'checkbox-multiple' === thisType ) {
+ jQuery( '#fb-checkbox-multiple-' + thisId + '-' + thisOptionid + ' span' ).text(
+ thisOptionValue
+ );
+ } else {
+ jQuery( '#fb-' + thisId + '-' + thisOptionid ).text( thisOptionValue );
+ }
+ // Update fbForm object
+ fbForm.fields[ thisId ].options[ thisOptionid ] = thisOptionValue;
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'updateOption(): ' + e );
+ }
+ }
+ }
+ function updateRequired() {
+ try {
+ var thisId = jQuery( '#fb-field-id' ).val();
+ var thisChecked = jQuery( '#fb-new-required' ).is( ':checked' );
+ // Update object and preview
+ if ( thisChecked ) {
+ fbForm.fields[ thisId ].required = true;
+ jQuery( '#fb-new-field' + thisId + ' label' ).append(
+ '<span class="label-required">' + GrunionFB_i18n.requiredLabel + '</span>'
+ );
+ } else {
+ fbForm.fields[ thisId ].required = false;
+ jQuery( '#fb-new-field' + thisId + ' label .label-required' ).remove();
+ }
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'updateRequired(): ' + e );
+ }
+ }
+ }
+ function updateSubject() {
+ try {
+ var thisSubject = jQuery( '#fb-field-subject' ).val();
+ fbForm.subject = thisSubject;
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'updateSubject(): ' + e );
+ }
+ }
+ }
+ function updateType( thisType, thisLabelText, thisRequired ) {
+ try {
+ var thisId = jQuery( '#fb-field-id' ).val();
+ if ( ! thisType ) {
+ thisType = jQuery( '#fb-new-type' ).val();
+ }
+ if ( ! thisLabelText ) {
+ thisLabelText = jQuery( '#fb-new-field' + thisId + ' .label-text' ).text();
+ }
+ var isRequired = thisRequired
+ ? '<span class="label-required">' + GrunionFB_i18n.requiredLabel + '</span>'
+ : '';
+ var thisLabel =
+ '<label fieldid="' +
+ thisId +
+ '" for="fb-field' +
+ thisId +
+ '"><span class="label-text">' +
+ FB.esc_html( thisLabelText ) +
+ '</span>' +
+ isRequired +
+ '</label>';
+ var thisRadio =
+ '<input type="radio" name="radio-' +
+ thisId +
+ '" id="fb-field' +
+ thisId +
+ ' "disabled="disabled" />';
+ var thisRadioLabel =
+ '<label fieldid="' +
+ thisId +
+ '" for="fb-field' +
+ thisId +
+ '" class="fb-radio-label"><span class="label-text">' +
+ FB.esc_html( thisLabelText ) +
+ '</span>' +
+ isRequired +
+ '</label>';
+ var thisRadioRemove = '<div class="fb-remove fb-remove-small" id="' + thisId + '"></div>';
+ var thisRemove = '<div class="fb-remove" id="' + thisId + '"></div>';
+ var thisCheckbox =
+ '<input type="checkbox" id="fb-field' + thisId + '" "disabled="disabled" />';
+ var thisCheckboxMultiple =
+ '<input type="checkbox" id="fb-field' + thisId + '" "disabled="disabled" />';
+ var thisCheckboxMultipleRemove =
+ '<div class="fb-remove fb-remove-small" id="' + thisId + '"></div>';
+ var thisText = '<input type="text" id="fb-field' + thisId + '" "disabled="disabled" />';
+ var thisTextarea = '<textarea id="fb-field' + thisId + '" "disabled="disabled"></textarea>';
+ var thisClear = '<div class="clear"></div>';
+ var thisSelect =
+ '<select id="fb-field' +
+ thisId +
+ '" fieldid="' +
+ thisId +
+ '"><option id="fb-' +
+ thisId +
+ '-' +
+ optionsCount +
+ '" value="' +
+ thisId +
+ '-' +
+ optionsCount +
+ '">' +
+ GrunionFB_i18n.firstOptionLabel +
+ '</option></select>';
+ switch ( thisType ) {
+ case 'checkbox':
+ removeOptions();
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).html(
+ thisRadioRemove + thisCheckbox + thisRadioLabel + thisClear
+ );
+ break;
+ case 'checkbox-multiple':
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).html(
+ thisLabel +
+ thisCheckboxMultipleRemove +
+ '<div fieldid="' +
+ thisId +
+ '" id="fb-custom-checkbox-multiple' +
+ thisId +
+ '"></div>'
+ );
+ if (
+ optionsCache[ thisId ] !== undefined &&
+ optionsCache[ thisId ].options.length !== 0
+ ) {
+ fbForm.fields[ thisId ].options = optionsCache[ thisId ].options;
+ jQuery( '#fb-custom-checkbox-multiple' + thisId ).append(
+ customOptions( thisId, thisType )
+ );
+ } else {
+ jQuery( '#fb-new-options' ).html(
+ '<label for="fb-option0">' +
+ GrunionFB_i18n.optionsLabel +
+ '</label><input type="text" id="fb-option0" optionid="0" value="' +
+ GrunionFB_i18n.firstOptionLabel +
+ '" class="fb-options" />'
+ );
+ jQuery( '#fb-custom-checkbox-multiple' + thisId ).append(
+ '<div id="fb-checkbox-multiple-' +
+ thisId +
+ '-0">' +
+ thisCheckboxMultiple +
+ '<span>' +
+ GrunionFB_i18n.firstOptionLabel +
+ '</span>' +
+ thisClear +
+ '</div>'
+ );
+ fbForm.fields[ thisId ].options[ optionsCount ] = GrunionFB_i18n.firstOptionLabel;
+ }
+ jQuery( '#fb-options' ).show();
+ setTimeout( function() {
+ jQuery( '#fb-option0' )
+ .focus()
+ .select();
+ }, 100 );
+ break;
+ case 'email':
+ removeOptions();
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).html(
+ thisRemove + thisLabel + thisText
+ );
+ break;
+ case 'name':
+ removeOptions();
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).html(
+ thisRemove + thisLabel + thisText
+ );
+ break;
+ case 'radio':
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).html(
+ thisLabel +
+ thisRadioRemove +
+ '<div fieldid="' +
+ thisId +
+ '" id="fb-custom-radio' +
+ thisId +
+ '"></div>'
+ );
+ if (
+ optionsCache[ thisId ] !== undefined &&
+ optionsCache[ thisId ].options.length !== 0
+ ) {
+ fbForm.fields[ thisId ].options = optionsCache[ thisId ].options;
+ jQuery( '#fb-custom-radio' + thisId ).append( customOptions( thisId, thisType ) );
+ } else {
+ jQuery( '#fb-new-options' ).html(
+ '<label for="fb-option0">' +
+ GrunionFB_i18n.optionsLabel +
+ '</label><input type="text" id="fb-option0" optionid="0" value="' +
+ GrunionFB_i18n.firstOptionLabel +
+ '" class="fb-options" />'
+ );
+ jQuery( '#fb-custom-radio' + thisId ).append(
+ '<div id="fb-radio-' +
+ thisId +
+ '-0">' +
+ thisRadio +
+ '<span>' +
+ GrunionFB_i18n.firstOptionLabel +
+ '</span>' +
+ thisClear +
+ '</div>'
+ );
+ fbForm.fields[ thisId ].options[ optionsCount ] = GrunionFB_i18n.firstOptionLabel;
+ }
+ jQuery( '#fb-options' ).show();
+ setTimeout( function() {
+ jQuery( '#fb-option0' )
+ .focus()
+ .select();
+ }, 100 );
+ break;
+ case 'select':
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).html(
+ thisRemove + thisLabel + thisSelect
+ );
+ if (
+ optionsCache[ thisId ] !== undefined &&
+ optionsCache[ thisId ].options.length !== 0
+ ) {
+ fbForm.fields[ thisId ].options = optionsCache[ thisId ].options;
+ jQuery( '#fb-field' + thisId ).html( customOptions( thisId, thisType ) );
+ } else {
+ jQuery( '#fb-new-options' ).html(
+ '<label for="fb-option0">' +
+ GrunionFB_i18n.optionsLabel +
+ '</label><input type="text" id="fb-option0" optionid="0" value="' +
+ GrunionFB_i18n.firstOptionLabel +
+ '" class="fb-options" />'
+ );
+ fbForm.fields[ thisId ].options[ optionsCount ] = GrunionFB_i18n.firstOptionLabel;
+ }
+ jQuery( '#fb-options' ).show();
+ setTimeout( function() {
+ jQuery( '#fb-option0' )
+ .focus()
+ .select();
+ }, 100 );
+ break;
+ case 'text':
+ removeOptions();
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).html(
+ thisRemove + thisLabel + thisText
+ );
+ break;
+ case 'textarea':
+ removeOptions();
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).html(
+ thisRemove + thisLabel + thisTextarea
+ );
+ break;
+ case 'url':
+ removeOptions();
+ jQuery( '#fb-new-field' + thisId + ' .fb-fields' ).html(
+ thisRemove + thisLabel + thisText
+ );
+ break;
+ }
+ // update object
+ fbForm.fields[ thisId ].type = thisType;
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'updateType(): ' + e );
+ }
+ }
+ }
+ return {
+ resizePop: function() {
+ try {
+ //Thickbox won't resize for some reason, we are manually doing it here
+ var totalWidth = jQuery( 'body', window.parent.document ).width();
+ var totalHeight = jQuery( 'body', window.parent.document ).height();
+ var isIE6 = typeof document.body.style.maxHeight === 'undefined';
+
+ jQuery( '#TB_window, #TB_iframeContent', window.parent.document ).css( 'width', '768px' );
+ jQuery( '#TB_window', window.parent.document ).css( {
+ left: ( totalWidth - 768 ) / 2 + 'px',
+ top: '23px',
+ position: 'absolute',
+ marginLeft: '0',
+ } );
+ if ( ! isIE6 ) {
+ // take away IE6
+ jQuery( '#TB_window, #TB_iframeContent', window.parent.document ).css(
+ 'height',
+ totalHeight - 73 + 'px'
+ );
+ }
+ } catch ( e ) {
+ if ( debug ) {
+ console.log( 'resizePop(): ' + e );
+ }
+ }
+ },
+ init: function() {
+ // Scroll to top of page
+ window.parent.scroll( 0, 0 );
+ //Check for existing form data
+ var contentSource;
+ if (
+ jQuery( '#edButtonPreview', window.parent.document ).hasClass( 'active' ) ||
+ jQuery( '#wp-content-wrap', window.parent.document ).hasClass( 'tmce-active' )
+ ) {
+ var win = window.dialogArguments || opener || parent || top;
+ contentSource = win.tinyMCE.activeEditor.getContent();
+ } else {
+ contentSource = jQuery( '#content', window.parent.document ).val();
+ }
+ var data = {
+ action: 'grunion_shortcode_to_json',
+ _ajax_nonce: ajax_nonce_json,
+ post_id: postId,
+ content: contentSource,
+ };
+
+ var $doc = jQuery( document );
+
+ jQuery.post( ajaxurl, data, function( response ) {
+ // Setup fbForm
+ parseShortcode( jQuery.parseJSON( response ) );
+ // Now build out the preview form
+ buildPreview();
+ } );
+ // actions
+ jQuery( '.fb-add-field' ).click( function() {
+ addField();
+ hideDesc();
+ return false;
+ } );
+ jQuery( '#fb-new-label' ).keyup( function() {
+ updateLabel();
+ } );
+ jQuery( '#fb-new-type' ).change( function() {
+ updateType();
+ } );
+ jQuery( '#fb-new-required' ).click( function() {
+ updateRequired();
+ } );
+ $doc.on( 'click', '.fb-remove', function() {
+ showDesc();
+ deleteField( jQuery( this ) );
+ grabShortcode();
+ } );
+ jQuery( '#fb-preview' ).submit( function() {
+ sendShortcodeToEditor();
+ return false;
+ } );
+ jQuery( '#TB_overlay, #TB_closeWindowButton', window.parent.document ).mousedown( function() {
+ if ( confirm( GrunionFB_i18n.exitConfirmMessage ) ) {
+ hidePopup();
+ }
+ } );
+ $doc.on( 'click', '#fb-another-option', function() {
+ addOption();
+ } );
+ $doc.on( 'keyup', '.fb-options', function() {
+ updateOption( jQuery( this ) );
+ } );
+ $doc.on( 'click', '.fb-remove-option', function() {
+ removeOption( jQuery( this ).attr( 'optionid' ) );
+ } );
+ jQuery( '#tab-preview a' ).click( function() {
+ switchTabs( 'preview' );
+ return false;
+ } );
+ jQuery( '#fb-prev-form' ).click( function() {
+ switchTabs( 'preview' );
+ return false;
+ } );
+ jQuery( '#tab-settings a' ).click( function() {
+ switchTabs();
+ return false;
+ } );
+ jQuery( '#fb-field-my-email' ).blur( function() {
+ updateMyEmail();
+ } );
+ jQuery( '#fb-field-subject' ).blur( function() {
+ updateSubject();
+ } );
+ $doc.on( 'mouseenter', '.fb-form-case .fb-new-fields', function() {
+ hideShowEditLink( 'show', jQuery( this ) );
+ } );
+ $doc.on( 'mouseleave', '.fb-form-case .fb-new-fields', function() {
+ hideShowEditLink( 'hide' );
+ return false;
+ } );
+ $doc.on( 'click', '.fb-edit-field', function() {
+ editField( jQuery( this ) );
+ return false;
+ } );
+ $doc.on( 'click', '.fb-edit-field .fb-reorder', function() {
+ return false;
+ } );
+ $doc.on( 'click', '#fb-save-field', function() {
+ showDesc();
+ showAndHideMessage();
+ return false;
+ } );
+ jQuery( '#fb-feedback' ).click( function() {
+ var thisHref = jQuery( this ).attr( 'href' );
+ window.parent.location = thisHref;
+ return false;
+ } );
+ jQuery( '#sortable' ).sortable( {
+ axis: 'y',
+ handle: '.fb-reorder',
+ revert: true,
+ start: function() {
+ jQuery( '.fb-edit-field' ).hide();
+ },
+ } );
+ jQuery( '#draggable' ).draggable( {
+ axis: 'y',
+ handle: '.fb-reorder',
+ connectToSortable: '#sortable',
+ helper: 'clone',
+ revert: 'invalid',
+ } );
+ },
+ };
+} )();
diff --git a/plugins/jetpack/modules/contact-form/js/tinymce-plugin-form-button.js b/plugins/jetpack/modules/contact-form/js/tinymce-plugin-form-button.js
new file mode 100644
index 00000000..8104ea32
--- /dev/null
+++ b/plugins/jetpack/modules/contact-form/js/tinymce-plugin-form-button.js
@@ -0,0 +1,37 @@
+/* global grunionEditorView, tinymce */
+( function() {
+ tinymce.create( 'tinymce.plugins.grunion_form', {
+ init: function( editor ) {
+ editor.addButton( 'grunion', {
+ title: grunionEditorView.labels.tinymce_label,
+ cmd: 'grunion_add_form',
+ icon: 'grunion',
+ } );
+ editor.addCommand( 'grunion_add_form', function() {
+ if ( grunionEditorView.default_form ) {
+ editor.execCommand(
+ 'mceInsertContent',
+ 0,
+ '[contact-form]' + grunionEditorView.default_form + '[/contact-form]'
+ );
+ } else {
+ editor.execCommand( 'mceInsertContent', 0, '[contact-form /]' );
+ }
+ } );
+ },
+
+ createControl: function() {
+ return null;
+ },
+
+ getInfo: function() {
+ return {
+ longname: 'Grunion Contact Form',
+ author: 'Automattic',
+ version: '1',
+ };
+ },
+ } );
+
+ tinymce.PluginManager.add( 'grunion_form', tinymce.plugins.grunion_form );
+} )();
diff --git a/plugins/jetpack/modules/copy-post.php b/plugins/jetpack/modules/copy-post.php
new file mode 100644
index 00000000..1ae33ced
--- /dev/null
+++ b/plugins/jetpack/modules/copy-post.php
@@ -0,0 +1,338 @@
+<?php
+/**
+ * Module Name: Copy Post
+ * Module Description: Copy an existing post's content into a new draft post
+ * Jumpstart Description: Copy an existing post's content into a new draft post
+ * Sort Order: 15
+ * First Introduced: 7.0
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Writing
+ * Feature: Writing
+ * Additional Search Queries: copy, duplicate
+ */
+
+/**
+ * Copy Post class.
+ */
+class Jetpack_Copy_Post {
+ /**
+ * Jetpack_Copy_Post_By_Param constructor.
+ * Add row actions to post/page/CPT listing screens.
+ * Process any `?copy` param if on a create new post/page/CPT screen.
+ *
+ * @return void
+ */
+ public function __construct() {
+ if ( 'edit.php' === $GLOBALS['pagenow'] ) {
+ add_filter( 'post_row_actions', array( $this, 'add_row_action' ), 10, 2 );
+ add_filter( 'page_row_actions', array( $this, 'add_row_action' ), 10, 2 );
+ return;
+ }
+
+ if ( ! empty( $_GET['jetpack-copy'] ) && 'post-new.php' === $GLOBALS['pagenow'] ) {
+ add_action( 'wp_insert_post', array( $this, 'update_post_data' ), 10, 3 );
+ add_filter( 'pre_option_default_post_format', '__return_empty_string' );
+ }
+ }
+
+ /**
+ * Update the new (target) post data with the source post data.
+ *
+ * @param int $target_post_id Target post ID.
+ * @param WP_Post $post Target post object (not used).
+ * @param bool $update Whether this is an existing post being updated or not.
+ * @return void
+ */
+ public function update_post_data( $target_post_id, $post, $update ) {
+ // This `$update` check avoids infinite loops of trying to update our updated post.
+ if ( $update ) {
+ return;
+ }
+
+ $source_post = get_post( $_GET['jetpack-copy'] );
+ if ( ! $source_post instanceof WP_Post ||
+ ! $this->user_can_access_post( $source_post->ID ) ||
+ ! $this->validate_post_type( $source_post ) ) {
+ return;
+ }
+
+ $update_results = array(
+ 'update_content' => $this->update_content( $source_post, $target_post_id ),
+ 'update_featured_image' => $this->update_featured_image( $source_post, $target_post_id ),
+ 'update_post_format' => $this->update_post_format( $source_post, $target_post_id ),
+ 'update_likes_sharing' => $this->update_likes_sharing( $source_post, $target_post_id ),
+ 'update_post_type_terms' => $this->update_post_type_terms( $source_post, $target_post_id ),
+ );
+
+ // Required to satisfy get_default_post_to_edit(), which has these filters after post creation.
+ add_filter( 'default_title', array( $this, 'filter_title' ), 10, 2 );
+ add_filter( 'default_content', array( $this, 'filter_content' ), 10, 2 );
+ add_filter( 'default_excerpt', array( $this, 'filter_excerpt' ), 10, 2 );
+
+ // Required to avoid the block editor from adding default blocks according to post format.
+ add_filter( 'block_editor_settings', array( $this, 'remove_post_format_template' ) );
+
+ /**
+ * Fires after all updates have been performed, and default content filters have been added.
+ * Allows for any cleanup or post operations, and default content filters can be removed or modified.
+ *
+ * @module copy-post
+ *
+ * @since 7.0.0
+ *
+ * @param WP_Post $source_post Post object that was copied.
+ * @param int $target_post_id Target post ID.
+ * @param array $update_results Results of all update operations, allowing action to be taken.
+ */
+ do_action( 'jetpack_copy_post', $source_post, $target_post_id, $update_results );
+ }
+
+ /**
+ * Determine if the current user has edit access to the source post.
+ *
+ * @param int $post_id Source post ID (the post being copied).
+ * @return bool True if user has the meta cap of `edit_post` for the given post ID, false otherwise.
+ */
+ protected function user_can_access_post( $post_id ) {
+ return current_user_can( 'edit_post', $post_id );
+ }
+
+ /**
+ * Update the target post's title, content, excerpt, categories, and tags.
+ *
+ * @param WP_Post $source_post Post object to be copied.
+ * @param int $target_post_id Target post ID.
+ * @return int 0 on failure, or the updated post ID on success.
+ */
+ protected function update_content( $source_post, $target_post_id ) {
+ $data = array(
+ 'ID' => $target_post_id,
+ 'post_title' => $source_post->post_title,
+ 'post_content' => $source_post->post_content,
+ 'post_excerpt' => $source_post->post_excerpt,
+ 'comment_status' => $source_post->comment_status,
+ 'ping_status' => $source_post->ping_status,
+ 'post_category' => $source_post->post_category,
+ 'post_password' => $source_post->post_password,
+ 'tags_input' => $source_post->tags_input,
+ );
+
+ /**
+ * Fires just before the target post is updated with its new data.
+ * Allows for final data adjustments before updating the target post.
+ *
+ * @module copy-post
+ *
+ * @since 7.0.0
+ *
+ * @param array $data Post data with which to update the target (new) post.
+ * @param WP_Post $source_post Post object being copied.
+ * @param int $target_post_id Target post ID.
+ */
+ $data = apply_filters( 'jetpack_copy_post_data', $data, $source_post, $target_post_id );
+ return wp_update_post( $data );
+ }
+
+ /**
+ * Update terms for post types.
+ *
+ * @param WP_Post $source_post Post object to be copied.
+ * @param int $target_post_id Target post ID.
+ * @return array Results of attempts to set each term to the target (new) post.
+ */
+ protected function update_post_type_terms( $source_post, $target_post_id ) {
+ $results = array();
+
+ $bypassed_post_types = apply_filters( 'jetpack_copy_post_bypassed_post_types', array( 'post', 'page' ), $source_post, $target_post_id );
+ if ( in_array( $source_post->post_type, $bypassed_post_types, true ) ) {
+ return $results;
+ }
+
+ $taxonomies = get_object_taxonomies( $source_post, 'objects' );
+ foreach ( $taxonomies as $taxonomy ) {
+ $terms = wp_get_post_terms( $source_post->ID, $taxonomy->name, array( 'fields' => 'ids' ) );
+ $results[] = wp_set_post_terms( $target_post_id, $terms, $taxonomy->name );
+ }
+
+ return $results;
+ }
+
+ /**
+ * Update the target post's featured image.
+ *
+ * @param WP_Post $source_post Post object to be copied.
+ * @param int $target_post_id Target post ID.
+ * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure.
+ */
+ protected function update_featured_image( $source_post, $target_post_id ) {
+ $featured_image_id = get_post_thumbnail_id( $source_post );
+ return update_post_meta( $target_post_id, '_thumbnail_id', $featured_image_id );
+ }
+
+ /**
+ * Update the target post's post format.
+ *
+ * @param WP_Post $source_post Post object to be copied.
+ * @param int $target_post_id Target post ID.
+ * @return array|WP_Error|false WP_Error on error, array of affected term IDs on success.
+ */
+ protected function update_post_format( $source_post, $target_post_id ) {
+ $post_format = get_post_format( $source_post );
+ return set_post_format( $target_post_id, $post_format );
+ }
+
+ /**
+ * Ensure the block editor doesn't modify the source post content for non-standard post formats.
+ *
+ * @param array $settings Settings to be passed into the block editor.
+ * @return array Settings with any `template` key removed.
+ */
+ public function remove_post_format_template( $settings ) {
+ unset( $settings['template'] );
+ return $settings;
+ }
+
+ /**
+ * Update the target post's Likes and Sharing statuses.
+ *
+ * @param WP_Post $source_post Post object to be copied.
+ * @param int $target_post_id Target post ID.
+ * @return array Array with the results of each update action.
+ */
+ protected function update_likes_sharing( $source_post, $target_post_id ) {
+ $likes = get_post_meta( $source_post->ID, 'switch_like_status', true );
+ $sharing = get_post_meta( $source_post->ID, 'sharing_disabled', false );
+ $likes_result = update_post_meta( $target_post_id, 'switch_like_status', $likes );
+ $sharing_result = update_post_meta( $target_post_id, 'sharing_disabled', $sharing );
+ return array(
+ 'likes' => $likes_result,
+ 'sharing' => $sharing_result,
+ );
+ }
+
+ /**
+ * Update the target post's title.
+ *
+ * @param string $post_title Post title determined by `get_default_post_to_edit()`.
+ * @param WP_Post $post Post object of newly-inserted post.
+ * @return string Updated post title from source post.
+ */
+ public function filter_title( $post_title, $post ) {
+ return $post->post_title;
+ }
+
+ /**
+ * Update the target post's content (`post_content`).
+ *
+ * @param string $post_content Post content determined by `get_default_post_to_edit()`.
+ * @param WP_Post $post Post object of newly-inserted post.
+ * @return string Updated post content from source post.
+ */
+ public function filter_content( $post_content, $post ) {
+ return $post->post_content;
+ }
+
+ /**
+ * Update the target post's excerpt.
+ *
+ * @param string $post_excerpt Post excerpt determined by `get_default_post_to_edit()`.
+ * @param WP_Post $post Post object of newly-inserted post.
+ * @return string Updated post excerpt from source post.
+ */
+ public function filter_excerpt( $post_excerpt, $post ) {
+ return $post->post_excerpt;
+ }
+
+ /**
+ * Validate the post type to be used for the target post.
+ *
+ * @param WP_Post $post Post object of current post in listing.
+ * @return bool True if the post type is in a list of supported psot types; false otherwise.
+ */
+ protected function validate_post_type( $post ) {
+ /**
+ * Fires when determining if the "Copy" row action should be made available.
+ * Allows overriding supported post types.
+ *
+ * @module copy-post
+ *
+ * @since 7.0.0
+ *
+ * @param array Post types supported by default.
+ * @param WP_Post $post Post object of current post in listing.
+ */
+ $valid_post_types = apply_filters(
+ 'jetpack_copy_post_post_types',
+ array(
+ 'post',
+ 'page',
+ 'jetpack-testimonial',
+ 'jetpack-portfolio',
+ ),
+ $post
+ );
+ return in_array( $post->post_type, $valid_post_types, true );
+ }
+
+ /**
+ * Add a "Copy" row action to supported posts/pages/CPTs on list views.
+ *
+ * @param array $actions Existing actions.
+ * @param WP_Post $post Post object of current post in list.
+ * @return array Array of updated row actions.
+ */
+ public function add_row_action( $actions, $post ) {
+ if ( ! $this->user_can_access_post( $post->ID ) ||
+ ! $post instanceof WP_Post ||
+ ! $this->validate_post_type( $post ) ) {
+ return $actions;
+ }
+
+ $edit_url = add_query_arg(
+ array(
+ 'post_type' => $post->post_type,
+ 'jetpack-copy' => $post->ID,
+ ),
+ admin_url( 'post-new.php' )
+ );
+ $edit_action = array(
+ 'jetpack-copy' => sprintf(
+ '<a href="%s" aria-label="%s">%s</a>',
+ esc_url( $edit_url ),
+ esc_attr__( 'Copy this post.', 'jetpack' ),
+ esc_html__( 'Copy', 'jetpack' )
+ ),
+ );
+
+ // Insert the Copy action before the Trash action.
+ $edit_offset = array_search( 'trash', array_keys( $actions ), true );
+ $updated_actions = array_merge(
+ array_slice( $actions, 0, $edit_offset ),
+ $edit_action,
+ array_slice( $actions, $edit_offset )
+ );
+
+ /**
+ * Fires after the new Copy action has been added to the row actions.
+ * Allows changes to the action presentation, or other final checks.
+ *
+ * @module copy-post
+ *
+ * @since 7.0.0
+ *
+ * @param array $updated_actions Updated row actions with the Copy Post action.
+ * @param array $actions Original row actions passed to this filter.
+ * @param WP_Post $post Post object of current post in listing.
+ */
+ return apply_filters( 'jetpack_copy_post_row_actions', $updated_actions, $actions, $post );
+ }
+}
+
+/**
+ * Instantiate an instance of Jetpack_Copy_Post on the `admin_init` hook.
+ */
+function jetpack_copy_post_init() {
+ new Jetpack_Copy_Post();
+}
+add_action( 'admin_init', 'jetpack_copy_post_init' );
diff --git a/plugins/jetpack/modules/custom-content-types.php b/plugins/jetpack/modules/custom-content-types.php
new file mode 100644
index 00000000..5976bfc1
--- /dev/null
+++ b/plugins/jetpack/modules/custom-content-types.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * Module Name: Custom content types
+ * Module Description: Display different types of content on your site with custom content types.
+ * First Introduced: 3.1
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Writing
+ * Sort Order: 34
+ * Feature: Writing
+ * Additional Search Queries: cpt, custom post types, portfolio, portfolios, testimonial, testimonials
+ */
+
+function jetpack_load_custom_post_types() {
+ include dirname( __FILE__ ) . "/custom-post-types/portfolios.php";
+}
+
+function jetpack_custom_post_types_loaded() {
+ Jetpack::enable_module_configurable( __FILE__ );
+}
+add_action( 'jetpack_modules_loaded', 'jetpack_custom_post_types_loaded' );
+
+// Add Settings Section for CPT
+function jetpack_cpt_settings_api_init() {
+ add_settings_section(
+ 'jetpack_cpt_section',
+ '<span id="cpt-options">' . __( 'Your Custom Content Types', 'jetpack' ) . '</span>',
+ 'jetpack_cpt_section_callback',
+ 'writing'
+ );
+}
+add_action( 'admin_init', 'jetpack_cpt_settings_api_init' );
+
+/*
+ * Settings Description
+ */
+function jetpack_cpt_section_callback() {
+ ?>
+ <p>
+ <?php esc_html_e( 'Use these settings to display different types of content on your site.', 'jetpack' ); ?>
+ <a target="_blank" href="http://jetpack.com/support/custom-content-types/"><?php esc_html_e( 'Learn More', 'jetpack' ); ?></a>
+ </p>
+ <?php
+}
+
+jetpack_load_custom_post_types();
diff --git a/plugins/jetpack/modules/custom-css.php b/plugins/jetpack/modules/custom-css.php
new file mode 100644
index 00000000..3ba63055
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * Module Name: Custom CSS
+ * Module Description: Adds options for CSS preprocessor use, disabling the theme's CSS, or custom image width.
+ * Sort Order: 2
+ * First Introduced: 1.7
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Appearance
+ * Feature: Appearance
+ * Additional Search Queries: css, customize, custom, style, editor, less, sass, preprocessor, font, mobile, appearance, theme, stylesheet
+ */
+
+function jetpack_load_custom_css() {
+ // If WordPress has the core version of Custom CSS, load our new version.
+ // @see https://core.trac.wordpress.org/changeset/38829
+ if ( function_exists( 'wp_get_custom_css' ) ) {
+ if ( ! function_exists( 'wp_update_custom_css_post' ) ) {
+ wp_die( 'Please run a SVN up to get the latest version of trunk, or update to at least 4.7 RC1' );
+ }
+ if ( ! Jetpack_Options::get_option( 'custom_css_4.7_migration' ) ) {
+ include_once dirname( __FILE__ ) . '/custom-css/migrate-to-core.php';
+ }
+
+ // TODO: DELETE THIS
+ else {
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
+ function jetpack_custom_css_undo_data_migration_cli() {
+ Jetpack_Options::delete_option( 'custom_css_4.7_migration' );
+ WP_CLI::success( __( 'Option deleted, re-migrate via `wp jetpack custom-css migrate`.', 'jetpack' ) );
+ }
+ WP_CLI::add_command( 'jetpack custom-css undo-migrate', 'jetpack_custom_css_undo_data_migration_cli' );
+ }
+ }
+ // TODO: END DELETE THIS
+
+ include_once dirname( __FILE__ ) . '/custom-css/custom-css/preprocessors.php';
+ include_once dirname( __FILE__ ) . '/custom-css/custom-css-4.7.php';
+ return;
+ }
+
+ include_once dirname( __FILE__ ) . "/custom-css/custom-css.php";
+ add_action( 'init', array( 'Jetpack_Custom_CSS', 'init' ) );
+}
+
+add_action( 'jetpack_modules_loaded', 'custom_css_loaded' );
+
+function custom_css_loaded() {
+ Jetpack::enable_module_configurable( __FILE__ );
+ add_filter( 'jetpack_module_configuration_url_custom-css', 'jetpack_custom_css_configuration_url' );
+
+}
+
+/**
+ * Overrides default configuration url
+ *
+ * @uses admin_url
+ * @return string module settings URL
+ */
+function jetpack_custom_css_configuration_url( $default_url ) {
+ return Jetpack_Custom_CSS_Enhancements::customizer_link(
+ array( 'return_url' => wp_get_referer() )
+ );
+}
+
+
+jetpack_load_custom_css();
diff --git a/plugins/jetpack/modules/custom-css/csstidy/class.csstidy.php b/plugins/jetpack/modules/custom-css/csstidy/class.csstidy.php
new file mode 100644
index 00000000..e92669da
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/class.csstidy.php
@@ -0,0 +1,1246 @@
+<?php
+/**
+ * CSSTidy - CSS Parser and Optimiser
+ *
+ * CSS Parser class
+ *
+ * Copyright 2005, 2006, 2007 Florian Schmitz
+ *
+ * This file is part of CSSTidy.
+ *
+ * CSSTidy is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * CSSTidy is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @license http://opensource.org/licenses/lgpl-license.php GNU Lesser General Public License
+ * @package csstidy
+ * @author Florian Schmitz (floele at gmail dot com) 2005-2007
+ * @author Brett Zamir (brettz9 at yahoo dot com) 2007
+ * @author Nikolay Matsievsky (speed at webo dot name) 2009-2010
+ * @author Cedric Morin (cedric at yterium dot com) 2010
+ */
+/**
+ * Defines ctype functions if required
+ *
+ * @version 1.0
+ */
+require_once( dirname( __FILE__ ) . '/class.csstidy_ctype.php' );
+
+/**
+ * Various CSS data needed for correct optimisations etc.
+ *
+ * @version 1.3
+ */
+require( dirname( __FILE__ ) . '/data.inc.php' );
+
+/**
+ * Contains a class for printing CSS code
+ *
+ * @version 1.0
+ */
+require( dirname( __FILE__ ) . '/class.csstidy_print.php' );
+
+/**
+ * Contains a class for optimising CSS code
+ *
+ * @version 1.0
+ */
+require( dirname( __FILE__ ) . '/class.csstidy_optimise.php' );
+
+/**
+ * CSS Parser class
+ *
+
+ * This class represents a CSS parser which reads CSS code and saves it in an array.
+ * In opposite to most other CSS parsers, it does not use regular expressions and
+ * thus has full CSS2 support and a higher reliability.
+ * Additional to that it applies some optimisations and fixes to the CSS code.
+ * An online version should be available here: http://cdburnerxp.se/cssparse/css_optimiser.php
+ * @package csstidy
+ * @author Florian Schmitz (floele at gmail dot com) 2005-2006
+ * @version 1.3.1
+ */
+class csstidy {
+
+ /**
+ * Saves the parsed CSS. This array is empty if preserve_css is on.
+ * @var array
+ * @access public
+ */
+ public $css = array();
+ /**
+ * Saves the parsed CSS (raw)
+ * @var array
+ * @access private
+ */
+ public $tokens = array();
+ /**
+ * Printer class
+ * @see csstidy_print
+ * @var object
+ * @access public
+ */
+ public $print;
+ /**
+ * Optimiser class
+ * @see csstidy_optimise
+ * @var object
+ * @access private
+ */
+ public $optimise;
+ /**
+ * Saves the CSS charset (@charset)
+ * @var string
+ * @access private
+ */
+ public $charset = '';
+ /**
+ * Saves all @import URLs
+ * @var array
+ * @access private
+ */
+ public $import = array();
+ /**
+ * Saves the namespace
+ * @var string
+ * @access private
+ */
+ public $namespace = '';
+ /**
+ * Contains the version of csstidy
+ * @var string
+ * @access private
+ */
+ public $version = '1.3';
+ /**
+ * Stores the settings
+ * @var array
+ * @access private
+ */
+ public $settings = array();
+ /**
+ * Saves the parser-status.
+ *
+ * Possible values:
+ * - is = in selector
+ * - ip = in property
+ * - iv = in value
+ * - instr = in string (started at " or ' or ( )
+ * - ic = in comment (ignore everything)
+ * - at = in @-block
+ *
+ * @var string
+ * @access private
+ */
+ public $status = 'is';
+ /**
+ * Saves the current at rule (@media)
+ * @var string
+ * @access private
+ */
+ public $at = '';
+ /**
+ * Saves the current selector
+ * @var string
+ * @access private
+ */
+ public $selector = '';
+ /**
+ * Saves the current property
+ * @var string
+ * @access private
+ */
+ public $property = '';
+ /**
+ * Saves the position of , in selectors
+ * @var array
+ * @access private
+ */
+ public $sel_separate = array();
+ /**
+ * Saves the current value
+ * @var string
+ * @access private
+ */
+ public $value = '';
+ /**
+ * Saves the current sub-value
+ *
+ * Example for a subvalue:
+ * background:url(foo.png) red no-repeat;
+ * "url(foo.png)", "red", and "no-repeat" are subvalues,
+ * separated by whitespace
+ * @var string
+ * @access private
+ */
+ public $sub_value = '';
+ /**
+ * Array which saves all subvalues for a property.
+ * @var array
+ * @see sub_value
+ * @access private
+ */
+ public $sub_value_arr = array();
+ /**
+ * Saves the stack of characters that opened the current strings
+ * @var array
+ * @access private
+ */
+ public $str_char = array();
+ public $cur_string = array();
+ /**
+ * Status from which the parser switched to ic or instr
+ * @var array
+ * @access private
+ */
+ public $from = array();
+ /**
+ /**
+ * =true if in invalid at-rule
+ * @var bool
+ * @access private
+ */
+ public $invalid_at = false;
+ /**
+ * =true if something has been added to the current selector
+ * @var bool
+ * @access private
+ */
+ public $added = false;
+ /**
+ * Array which saves the message log
+ * @var array
+ * @access private
+ */
+ public $log = array();
+ /**
+ * Saves the line number
+ * @var integer
+ * @access private
+ */
+ public $line = 1;
+ /**
+ * Marks if we need to leave quotes for a string
+ * @var array
+ * @access private
+ */
+ public $quoted_string = array();
+
+ /**
+ * List of tokens
+ * @var string
+ */
+ public $tokens_list = "";
+
+ /**
+ * Loads standard template and sets default settings
+ * @access private
+ * @version 1.3
+ */
+ function __construct() {
+ $this->settings['remove_bslash'] = true;
+ $this->settings['compress_colors'] = true;
+ $this->settings['compress_font-weight'] = true;
+ $this->settings['lowercase_s'] = false;
+ /*
+ 1 common shorthands optimization
+ 2 + font property optimization
+ 3 + background property optimization
+ */
+ $this->settings['optimise_shorthands'] = 1;
+ $this->settings['remove_last_;'] = true;
+ /* rewrite all properties with low case, better for later gzip OK, safe*/
+ $this->settings['case_properties'] = 1;
+ /* sort properties in alpabetic order, better for later gzip
+ * but can cause trouble in case of overiding same propertie or using hack
+ */
+ $this->settings['sort_properties'] = false;
+ /*
+ 1, 3, 5, etc -- enable sorting selectors inside @media: a{}b{}c{}
+ 2, 5, 8, etc -- enable sorting selectors inside one CSS declaration: a,b,c{}
+ preserve order by default cause it can break functionnality
+ */
+ $this->settings['sort_selectors'] = 0;
+ /* is dangeroues to be used: CSS is broken sometimes */
+ $this->settings['merge_selectors'] = 0;
+ /* preserve or not browser hacks */
+ $this->settings['discard_invalid_selectors'] = false;
+ $this->settings['discard_invalid_properties'] = false;
+ $this->settings['css_level'] = 'CSS2.1';
+ $this->settings['preserve_css'] = false;
+ $this->settings['timestamp'] = false;
+ $this->settings['template'] = ''; // say that propertie exist
+ $this->set_cfg('template','default'); // call load_template
+ $this->optimise = new csstidy_optimise($this);
+
+ $this->tokens_list = & $GLOBALS['csstidy']['tokens'];
+ }
+
+ function csstidy() {
+ $this->__construct();
+ }
+
+ /**
+ * Get the value of a setting.
+ * @param string $setting
+ * @access public
+ * @return mixed
+ * @version 1.0
+ */
+ function get_cfg($setting) {
+ if (isset($this->settings[$setting])) {
+ return $this->settings[$setting];
+ }
+ return false;
+ }
+
+ /**
+ * Load a template
+ * @param string $template used by set_cfg to load a template via a configuration setting
+ * @access private
+ * @version 1.4
+ */
+ function _load_template($template) {
+ switch ($template) {
+ case 'default':
+ $this->load_template('default');
+ break;
+
+ case 'highest':
+ $this->load_template('highest_compression');
+ break;
+
+ case 'high':
+ $this->load_template('high_compression');
+ break;
+
+ case 'low':
+ $this->load_template('low_compression');
+ break;
+
+ default:
+ $this->load_template($template);
+ break;
+ }
+ }
+
+ /**
+ * Set the value of a setting.
+ * @param string $setting
+ * @param mixed $value
+ * @access public
+ * @return bool
+ * @version 1.0
+ */
+ function set_cfg($setting, $value=null) {
+ if (is_array($setting) && $value === null) {
+ foreach ($setting as $setprop => $setval) {
+ $this->settings[$setprop] = $setval;
+ }
+ if (array_key_exists('template', $setting)) {
+ $this->_load_template($this->settings['template']);
+ }
+ return true;
+ } else if (isset($this->settings[$setting]) && $value !== '') {
+ $this->settings[$setting] = $value;
+ if ($setting === 'template') {
+ $this->_load_template($this->settings['template']);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Adds a token to $this->tokens
+ * @param mixed $type
+ * @param string $data
+ * @param bool $do add a token even if preserve_css is off
+ * @access private
+ * @version 1.0
+ */
+ function _add_token($type, $data, $do = false) {
+ if ($this->get_cfg('preserve_css') || $do) {
+ $this->tokens[] = array($type, ($type == COMMENT) ? $data : trim($data));
+ }
+ }
+
+ /**
+ * Add a message to the message log
+ * @param string $message
+ * @param string $type
+ * @param integer $line
+ * @access private
+ * @version 1.0
+ */
+ function log($message, $type, $line = -1) {
+ if ($line === -1) {
+ $line = $this->line;
+ }
+ $line = intval($line);
+ $add = array('m' => $message, 't' => $type);
+ if (!isset($this->log[$line]) || !in_array($add, $this->log[$line])) {
+ $this->log[$line][] = $add;
+ }
+ }
+
+ /**
+ * Parse unicode notations and find a replacement character
+ * @param string $string
+ * @param integer $i
+ * @access private
+ * @return string
+ * @version 1.2
+ */
+ function _unicode(&$string, &$i) {
+ ++$i;
+ $add = '';
+ $replaced = false;
+
+ while ($i < strlen($string) && (ctype_xdigit($string{$i}) || ctype_space($string{$i})) && strlen($add) < 6) {
+ $add .= $string{$i};
+
+ if (ctype_space($string{$i})) {
+ break;
+ }
+ $i++;
+ }
+
+ if (hexdec($add) > 47 && hexdec($add) < 58 || hexdec($add) > 64 && hexdec($add) < 91 || hexdec($add) > 96 && hexdec($add) < 123) {
+ $this->log('Replaced unicode notation: Changed \\' . $add . ' to ' . chr(hexdec($add)), 'Information');
+ $add = chr(hexdec($add));
+ $replaced = true;
+ } else {
+ $add = trim('\\' . $add);
+ }
+
+ if (@ctype_xdigit($string{$i + 1}) && ctype_space($string{$i})
+ && !$replaced || !ctype_space($string{$i})) {
+ $i--;
+ }
+
+ if ($add !== '\\' || !$this->get_cfg('remove_bslash') || strpos($this->tokens_list, $string{$i + 1}) !== false) {
+ return $add;
+ }
+
+ if ($add === '\\') {
+ $this->log('Removed unnecessary backslash', 'Information');
+ }
+ return '';
+ }
+
+ /**
+ * Write formatted output to a file
+ * @param string $filename
+ * @param string $doctype when printing formatted, is a shorthand for the document type
+ * @param bool $externalcss when printing formatted, indicates whether styles to be attached internally or as an external stylesheet
+ * @param string $title when printing formatted, is the title to be added in the head of the document
+ * @param string $lang when printing formatted, gives a two-letter language code to be added to the output
+ * @access public
+ * @version 1.4
+ */
+ function write_page($filename, $doctype='xhtml1.1', $externalcss=true, $title='', $lang='en') {
+ $this->write($filename, true);
+ }
+
+ /**
+ * Write plain output to a file
+ * @param string $filename
+ * @param bool $formatted whether to print formatted or not
+ * @param string $doctype when printing formatted, is a shorthand for the document type
+ * @param bool $externalcss when printing formatted, indicates whether styles to be attached internally or as an external stylesheet
+ * @param string $title when printing formatted, is the title to be added in the head of the document
+ * @param string $lang when printing formatted, gives a two-letter language code to be added to the output
+ * @param bool $pre_code whether to add pre and code tags around the code (for light HTML formatted templates)
+ * @access public
+ * @version 1.4
+ */
+ function write($filename, $formatted=false, $doctype='xhtml1.1', $externalcss=true, $title='', $lang='en', $pre_code=true) {
+ $filename .= ( $formatted) ? '.xhtml' : '.css';
+
+ if (!is_dir('temp')) {
+ $madedir = mkdir('temp');
+ if (!$madedir) {
+ print 'Could not make directory "temp" in ' . dirname(__FILE__);
+ exit;
+ }
+ }
+ $handle = fopen('temp/' . $filename, 'w');
+ if ($handle) {
+ if (!$formatted) {
+ fwrite($handle, $this->print->plain());
+ } else {
+ fwrite($handle, $this->print->formatted_page($doctype, $externalcss, $title, $lang, $pre_code));
+ }
+ }
+ fclose($handle);
+ }
+
+ /**
+ * Loads a new template
+ * @param string $content either filename (if $from_file == true), content of a template file, "high_compression", "highest_compression", "low_compression", or "default"
+ * @param bool $from_file uses $content as filename if true
+ * @access public
+ * @version 1.1
+ * @see http://csstidy.sourceforge.net/templates.php
+ */
+ function load_template($content, $from_file=true) {
+ $predefined_templates = & $GLOBALS['csstidy']['predefined_templates'];
+ if ($content === 'high_compression' || $content === 'default' || $content === 'highest_compression' || $content === 'low_compression') {
+ $this->template = $predefined_templates[$content];
+ return;
+ }
+
+
+ if ($from_file) {
+ $content = strip_tags(file_get_contents($content), '<span>');
+ }
+ $content = str_replace("\r\n", "\n", $content); // Unify newlines (because the output also only uses \n)
+ $template = explode('|', $content);
+
+ for ($i = 0; $i < count($template); $i++) {
+ $this->template[$i] = $template[$i];
+ }
+ }
+
+ /**
+ * Starts parsing from URL
+ * @param string $url
+ * @access public
+ * @version 1.0
+ */
+ function parse_from_url($url) {
+ return $this->parse(@file_get_contents($url));
+ }
+
+ /**
+ * Checks if there is a token at the current position
+ * @param string $string
+ * @param integer $i
+ * @access public
+ * @version 1.11
+ */
+ function is_token(&$string, $i) {
+ return (strpos($this->tokens_list, $string{$i}) !== false && !csstidy::escaped($string, $i));
+ }
+
+ /**
+ * Parses CSS in $string. The code is saved as array in $this->css
+ * @param string $string the CSS code
+ * @access public
+ * @return bool
+ * @version 1.1
+ */
+ function parse($string) {
+ // Temporarily set locale to en_US in order to handle floats properly
+ $old = @setlocale(LC_ALL, 0);
+ @setlocale(LC_ALL, 'C');
+
+ // PHP bug? Settings need to be refreshed in PHP4
+ $this->print = new csstidy_print($this);
+ //$this->optimise = new csstidy_optimise($this);
+
+ $all_properties = & $GLOBALS['csstidy']['all_properties'];
+ $at_rules = & $GLOBALS['csstidy']['at_rules'];
+ $quoted_string_properties = & $GLOBALS['csstidy']['quoted_string_properties'];
+
+ $this->css = array();
+ $this->print->input_css = $string;
+ $string = str_replace("\r\n", "\n", $string) . ' ';
+ $cur_comment = '';
+
+ for ($i = 0, $size = strlen($string); $i < $size; $i++) {
+ if ($string{$i} === "\n" || $string{$i} === "\r") {
+ ++$this->line;
+ }
+
+ switch ($this->status) {
+ /* Case in at-block */
+ case 'at':
+ if (csstidy::is_token($string, $i)) {
+ if ($string{$i} === '/' && @$string{$i + 1} === '*') {
+ $this->status = 'ic';
+ ++$i;
+ $this->from[] = 'at';
+ } elseif ($string{$i} === '{') {
+ $this->status = 'is';
+ $this->at = $this->css_new_media_section($this->at);
+ $this->_add_token(AT_START, $this->at);
+ } elseif ($string{$i} === ',') {
+ $this->at = trim($this->at) . ',';
+ } elseif ($string{$i} === '\\') {
+ $this->at .= $this->_unicode($string, $i);
+ }
+ // fix for complicated media, i.e @media screen and (-webkit-min-device-pixel-ratio:1.5)
+ // '/' is included for ratios in Opera: (-o-min-device-pixel-ratio: 3/2)
+ elseif (in_array($string{$i}, array('(', ')', ':', '.', '/'))) {
+ $this->at .= $string{$i};
+ }
+ } else {
+ $lastpos = strlen($this->at) - 1;
+ if (!( (ctype_space($this->at{$lastpos}) || csstidy::is_token($this->at, $lastpos) && $this->at{$lastpos} === ',') && ctype_space($string{$i}))) {
+ $this->at .= $string{$i};
+ }
+ }
+ break;
+
+ /* Case in-selector */
+ case 'is':
+ if (csstidy::is_token($string, $i)) {
+ if ($string{$i} === '/' && @$string{$i + 1} === '*' && trim($this->selector) == '') {
+ $this->status = 'ic';
+ ++$i;
+ $this->from[] = 'is';
+ } elseif ($string{$i} === '@' && trim($this->selector) == '') {
+ // Check for at-rule
+ $this->invalid_at = true;
+ foreach ($at_rules as $name => $type) {
+ if (!strcasecmp(substr($string, $i + 1, strlen($name)), $name)) {
+ ($type === 'at') ? $this->at = '@' . $name : $this->selector = '@' . $name;
+ $this->status = $type;
+ $i += strlen($name);
+ $this->invalid_at = false;
+ }
+ }
+
+ if ($this->invalid_at) {
+ $this->selector = '@';
+ $invalid_at_name = '';
+ for ($j = $i + 1; $j < $size; ++$j) {
+ if (!ctype_alpha($string{$j})) {
+ break;
+ }
+ $invalid_at_name .= $string{$j};
+ }
+ $this->log('Invalid @-rule: ' . $invalid_at_name . ' (removed)', 'Warning');
+ }
+ } elseif (($string{$i} === '"' || $string{$i} === "'")) {
+ $this->cur_string[] = $string{$i};
+ $this->status = 'instr';
+ $this->str_char[] = $string{$i};
+ $this->from[] = 'is';
+ /* fixing CSS3 attribute selectors, i.e. a[href$=".mp3" */
+ $this->quoted_string[] = ($string{$i - 1} == '=' );
+ } elseif ($this->invalid_at && $string{$i} === ';') {
+ $this->invalid_at = false;
+ $this->status = 'is';
+ } elseif ($string{$i} === '{') {
+ $this->status = 'ip';
+ if($this->at == '') {
+ $this->at = $this->css_new_media_section(DEFAULT_AT);
+ }
+ $this->selector = $this->css_new_selector($this->at,$this->selector);
+ $this->_add_token(SEL_START, $this->selector);
+ $this->added = false;
+ } elseif ($string{$i} === '}') {
+ $this->_add_token(AT_END, $this->at);
+ $this->at = '';
+ $this->selector = '';
+ $this->sel_separate = array();
+ } elseif ($string{$i} === ',') {
+ $this->selector = trim($this->selector) . ',';
+ $this->sel_separate[] = strlen($this->selector);
+ } elseif ($string{$i} === '\\') {
+ $this->selector .= $this->_unicode($string, $i);
+ } elseif ($string{$i} === '*' && @in_array($string{$i + 1}, array('.', '#', '[', ':'))) {
+ // remove unnecessary universal selector, FS#147
+ } else {
+ $this->selector .= $string{$i};
+ }
+ } else {
+ $lastpos = strlen($this->selector) - 1;
+ if ($lastpos == -1 || !( (ctype_space($this->selector{$lastpos}) || csstidy::is_token($this->selector, $lastpos) && $this->selector{$lastpos} === ',') && ctype_space($string{$i}))) {
+ $this->selector .= $string{$i};
+ }
+ else if (ctype_space($string{$i}) && $this->get_cfg('preserve_css') && !$this->get_cfg('merge_selectors')) {
+ $this->selector .= $string{$i};
+ }
+ }
+ break;
+
+ /* Case in-property */
+ case 'ip':
+ if (csstidy::is_token($string, $i)) {
+ if (($string{$i} === ':' || $string{$i} === '=') && $this->property != '') {
+ $this->status = 'iv';
+ if (!$this->get_cfg('discard_invalid_properties') || csstidy::property_is_valid($this->property)) {
+ $this->property = $this->css_new_property($this->at,$this->selector,$this->property);
+ $this->_add_token(PROPERTY, $this->property);
+ }
+ } elseif ($string{$i} === '/' && @$string{$i + 1} === '*' && $this->property == '') {
+ $this->status = 'ic';
+ ++$i;
+ $this->from[] = 'ip';
+ } elseif ($string{$i} === '}') {
+ $this->explode_selectors();
+ $this->status = 'is';
+ $this->invalid_at = false;
+ $this->_add_token(SEL_END, $this->selector);
+ $this->selector = '';
+ $this->property = '';
+ } elseif ($string{$i} === ';') {
+ $this->property = '';
+ } elseif ($string{$i} === '\\') {
+ $this->property .= $this->_unicode($string, $i);
+ }
+ // else this is dumb IE a hack, keep it
+ elseif ($this->property=='' AND !ctype_space($string{$i})) {
+ $this->property .= $string{$i};
+ }
+ }
+ elseif (!ctype_space($string{$i})) {
+ $this->property .= $string{$i};
+ }
+ break;
+
+ /* Case in-value */
+ case 'iv':
+ $pn = (($string{$i} === "\n" || $string{$i} === "\r") && $this->property_is_next($string, $i + 1) || $i == strlen($string) - 1);
+ if ((csstidy::is_token($string, $i) || $pn) && (!($string{$i} == ',' && !ctype_space($string{$i+1})))) {
+ if ($string{$i} === '/' && @$string{$i + 1} === '*') {
+ $this->status = 'ic';
+ ++$i;
+ $this->from[] = 'iv';
+ } elseif (($string{$i} === '"' || $string{$i} === "'" || $string{$i} === '(')) {
+ $this->cur_string[] = $string{$i};
+ $this->str_char[] = ($string{$i} === '(') ? ')' : $string{$i};
+ $this->status = 'instr';
+ $this->from[] = 'iv';
+ $this->quoted_string[] = in_array(strtolower($this->property), $quoted_string_properties);
+ } elseif ($string{$i} === ',') {
+ $this->sub_value = trim($this->sub_value) . ',';
+ } elseif ($string{$i} === '\\') {
+ $this->sub_value .= $this->_unicode($string, $i);
+ } elseif ($string{$i} === ';' || $pn) {
+ if ($this->selector{0} === '@' && isset($at_rules[substr($this->selector, 1)]) && $at_rules[substr($this->selector, 1)] === 'iv') {
+ $this->status = 'is';
+
+ switch ($this->selector) {
+ case '@charset':
+ /* Add quotes to charset */
+ $this->sub_value_arr[] = '"' . trim($this->sub_value) . '"';
+ $this->charset = $this->sub_value_arr[0];
+ break;
+ case '@namespace':
+ /* Add quotes to namespace */
+ $this->sub_value_arr[] = '"' . trim($this->sub_value) . '"';
+ $this->namespace = implode(' ', $this->sub_value_arr);
+ break;
+ case '@import':
+ $this->sub_value = trim($this->sub_value);
+
+ if (empty($this->sub_value_arr)) {
+ // Quote URLs in imports only if they're not already inside url() and not already quoted.
+ if (substr($this->sub_value, 0, 4) != 'url(') {
+ if (!($this->sub_value{0} == substr($this->sub_value, -1) && in_array($this->sub_value{0}, array("'", '"')))) {
+ $this->sub_value = '"' . $this->sub_value . '"';
+ }
+ }
+ }
+
+ $this->sub_value_arr[] = $this->sub_value;
+ $this->import[] = implode(' ', $this->sub_value_arr);
+ break;
+ }
+
+ $this->sub_value_arr = array();
+ $this->sub_value = '';
+ $this->selector = '';
+ $this->sel_separate = array();
+ } else {
+ $this->status = 'ip';
+ }
+ } elseif ($string{$i} !== '}') {
+ $this->sub_value .= $string{$i};
+ }
+ if (($string{$i} === '}' || $string{$i} === ';' || $pn) && !empty($this->selector)) {
+ if ($this->at == '') {
+ $this->at = $this->css_new_media_section(DEFAULT_AT);
+ }
+
+ // case settings
+ if ($this->get_cfg('lowercase_s')) {
+ $this->selector = strtolower($this->selector);
+ }
+ $this->property = strtolower($this->property);
+
+ $this->optimise->subvalue();
+ if ($this->sub_value != '') {
+ if (substr($this->sub_value, 0, 6) == 'format') {
+ $format_strings = csstidy::parse_string_list(substr($this->sub_value, 7, -1));
+ if (!$format_strings) {
+ $this->sub_value = "";
+ }
+ else {
+ $this->sub_value = "format(";
+
+ foreach ($format_strings as $format_string) {
+ $this->sub_value .= '"' . str_replace('"', '\\"', $format_string) . '",';
+ }
+
+ $this->sub_value = substr($this->sub_value, 0, -1) . ")";
+ }
+ }
+ if ($this->sub_value != '') {
+ $this->sub_value_arr[] = $this->sub_value;
+ }
+ $this->sub_value = '';
+ }
+
+ $this->value = array_shift($this->sub_value_arr);
+ while(count($this->sub_value_arr)){
+ //$this->value .= (substr($this->value,-1,1)==','?'':' ').array_shift($this->sub_value_arr);
+ $this->value .= ' '.array_shift($this->sub_value_arr);
+ }
+
+ $this->optimise->value();
+
+ $valid = csstidy::property_is_valid($this->property);
+ if ((!$this->invalid_at || $this->get_cfg('preserve_css')) && (!$this->get_cfg('discard_invalid_properties') || $valid)) {
+ $this->css_add_property($this->at, $this->selector, $this->property, $this->value);
+ $this->_add_token(VALUE, $this->value);
+ $this->optimise->shorthands();
+ }
+ if (!$valid) {
+ if ($this->get_cfg('discard_invalid_properties')) {
+ $this->log('Removed invalid property: ' . $this->property, 'Warning');
+ } else {
+ $this->log('Invalid property in ' . strtoupper($this->get_cfg('css_level')) . ': ' . $this->property, 'Warning');
+ }
+ }
+
+ $this->property = '';
+ $this->sub_value_arr = array();
+ $this->value = '';
+ }
+ if ($string{$i} === '}') {
+ $this->explode_selectors();
+ $this->_add_token(SEL_END, $this->selector);
+ $this->status = 'is';
+ $this->invalid_at = false;
+ $this->selector = '';
+ }
+ } elseif (!$pn) {
+ $this->sub_value .= $string{$i};
+
+ if (ctype_space($string{$i}) || $string{$i} == ',') {
+ $this->optimise->subvalue();
+ if ($this->sub_value != '') {
+ $this->sub_value_arr[] = $this->sub_value;
+ $this->sub_value = '';
+ }
+ }
+ }
+ break;
+
+ /* Case in string */
+ case 'instr':
+ $_str_char = $this->str_char[count($this->str_char)-1];
+ $_cur_string = $this->cur_string[count($this->cur_string)-1];
+ $temp_add = $string{$i};
+
+ // Add another string to the stack. Strings can't be nested inside of quotes, only parentheses, but
+ // parentheticals can be nested more than once.
+ if ($_str_char === ")" && ($string{$i} === "(" || $string{$i} === '"' || $string{$i} === '\'') && !csstidy::escaped($string, $i)) {
+ $this->cur_string[] = $string{$i};
+ $this->str_char[] = $string{$i} == "(" ? ")" : $string{$i};
+ $this->from[] = 'instr';
+ $this->quoted_string[] = !($string{$i} === "(");
+ continue 2;
+ }
+
+ if ($_str_char !== ")" && ($string{$i} === "\n" || $string{$i} === "\r") && !($string{$i - 1} === '\\' && !csstidy::escaped($string, $i - 1))) {
+ $temp_add = "\\A";
+ $this->log('Fixed incorrect newline in string', 'Warning');
+ }
+
+ $_cur_string .= $temp_add;
+
+ if ($string{$i} === $_str_char && !csstidy::escaped($string, $i)) {
+ $_quoted_string = array_pop($this->quoted_string);
+
+ $this->status = array_pop($this->from);
+
+ if (!preg_match('|[' . implode('', $GLOBALS['csstidy']['whitespace']) . ']|uis', $_cur_string) && $this->property !== 'content') {
+ if (!$_quoted_string) {
+ if ($_str_char !== ')') {
+ // Convert properties like
+ // font-family: 'Arial';
+ // to
+ // font-family: Arial;
+ // or
+ // url("abc")
+ // to
+ // url(abc)
+ $_cur_string = substr($_cur_string, 1, -1);
+ }
+ } else {
+ $_quoted_string = false;
+ }
+ }
+
+ array_pop($this->cur_string);
+ array_pop($this->str_char);
+
+ if ($_str_char === ")") {
+ $_cur_string = "(" . trim(substr($_cur_string, 1, -1)) . ")";
+ }
+
+ if ($this->status === 'iv') {
+ if (!$_quoted_string){
+ if (strpos($_cur_string,',')!==false)
+ // we can on only remove space next to ','
+ $_cur_string = implode(',',array_map('trim',explode(',',$_cur_string)));
+ // and multiple spaces (too expensive)
+ if (strpos($_cur_string,' ')!==false)
+ $_cur_string = preg_replace(",\s+,"," ",$_cur_string);
+ }
+ $this->sub_value .= $_cur_string;
+ } elseif ($this->status === 'is') {
+ $this->selector .= $_cur_string;
+ } elseif ($this->status === 'instr') {
+ $this->cur_string[count($this->cur_string)-1] .= $_cur_string;
+ }
+ }
+ else {
+ $this->cur_string[count($this->cur_string)-1] = $_cur_string;
+ }
+ break;
+
+ /* Case in-comment */
+ case 'ic':
+ if ($string{$i} === '*' && $string{$i + 1} === '/') {
+ $this->status = array_pop($this->from);
+ $i++;
+ $this->_add_token(COMMENT, $cur_comment);
+ $cur_comment = '';
+ } else {
+ $cur_comment .= $string{$i};
+ }
+ break;
+ }
+ }
+
+ $this->optimise->postparse();
+
+ $this->print->_reset();
+
+ @setlocale(LC_ALL, $old); // Set locale back to original setting
+
+ return!(empty($this->css) && empty($this->import) && empty($this->charset) && empty($this->tokens) && empty($this->namespace));
+ }
+
+ /**
+ * Explodes selectors
+ * @access private
+ * @version 1.0
+ */
+ function explode_selectors() {
+ // Explode multiple selectors
+ if ($this->get_cfg('merge_selectors') === 1) {
+ $new_sels = array();
+ $lastpos = 0;
+ $this->sel_separate[] = strlen($this->selector);
+ foreach ($this->sel_separate as $num => $pos) {
+ if ($num == count($this->sel_separate) - 1) {
+ $pos += 1;
+ }
+
+ $new_sels[] = substr($this->selector, $lastpos, $pos - $lastpos - 1);
+ $lastpos = $pos;
+ }
+
+ if (count($new_sels) > 1) {
+ foreach ($new_sels as $selector) {
+ if (isset($this->css[$this->at][$this->selector])) {
+ $this->merge_css_blocks($this->at, $selector, $this->css[$this->at][$this->selector]);
+ }
+ }
+ unset($this->css[$this->at][$this->selector]);
+ }
+ }
+ $this->sel_separate = array();
+ }
+
+ /**
+ * Checks if a character is escaped (and returns true if it is)
+ * @param string $string
+ * @param integer $pos
+ * @access public
+ * @return bool
+ * @version 1.02
+ */
+ static function escaped(&$string, $pos) {
+ return!(@($string{$pos - 1} !== '\\') || csstidy::escaped($string, $pos - 1));
+ }
+
+ /**
+ * Adds a property with value to the existing CSS code
+ * @param string $media
+ * @param string $selector
+ * @param string $property
+ * @param string $new_val
+ * @access private
+ * @version 1.2
+ */
+ function css_add_property($media, $selector, $property, $new_val) {
+ if ($this->get_cfg('preserve_css') || trim($new_val) == '') {
+ return;
+ }
+
+ $this->added = true;
+ if (isset($this->css[$media][$selector][$property])) {
+ if ((csstidy::is_important($this->css[$media][$selector][$property]) && csstidy::is_important($new_val)) || !csstidy::is_important($this->css[$media][$selector][$property])) {
+ $this->css[$media][$selector][$property] = trim($new_val);
+ }
+ } else {
+ $this->css[$media][$selector][$property] = trim($new_val);
+ }
+ }
+
+ /**
+ * Start a new media section.
+ * Check if the media is not already known,
+ * else rename it with extra spaces
+ * to avoid merging
+ *
+ * @param string $media
+ * @return string
+ */
+ function css_new_media_section($media){
+ if($this->get_cfg('preserve_css')) {
+ return $media;
+ }
+
+ // if the last @media is the same as this
+ // keep it
+ if (!$this->css OR !is_array($this->css) OR empty($this->css)){
+ return $media;
+ }
+ end($this->css);
+ $at = current( $this->css );
+ if ($at == $media){
+ return $media;
+ }
+ while (isset($this->css[$media]))
+ if (is_numeric($media))
+ $media++;
+ else
+ $media .= " ";
+ return $media;
+ }
+
+ /**
+ * Start a new selector.
+ * If already referenced in this media section,
+ * rename it with extra space to avoid merging
+ * except if merging is required,
+ * or last selector is the same (merge siblings)
+ *
+ * never merge @font-face
+ *
+ * @param string $media
+ * @param string $selector
+ * @return string
+ */
+ function css_new_selector($media,$selector){
+ if($this->get_cfg('preserve_css')) {
+ return $selector;
+ }
+ $selector = trim($selector);
+ if (strncmp($selector,"@font-face",10)!=0){
+ if ($this->settings['merge_selectors'] != false)
+ return $selector;
+
+ if (!$this->css OR !isset($this->css[$media]) OR !$this->css[$media])
+ return $selector;
+
+ // if last is the same, keep it
+ end($this->css[$media]);
+ $sel = current( $this->css[$media] );
+ if ($sel == $selector){
+ return $selector;
+ }
+ }
+
+ while (isset($this->css[$media][$selector]))
+ $selector .= " ";
+ return $selector;
+ }
+
+ /**
+ * Start a new propertie.
+ * If already references in this selector,
+ * rename it with extra space to avoid override
+ *
+ * @param string $media
+ * @param string $selector
+ * @param string $property
+ * @return string
+ */
+ function css_new_property($media, $selector, $property){
+ if($this->get_cfg('preserve_css')) {
+ return $property;
+ }
+ if (!$this->css OR !isset($this->css[$media][$selector]) OR !$this->css[$media][$selector])
+ return $property;
+
+ while (isset($this->css[$media][$selector][$property]))
+ $property .= " ";
+
+ return $property;
+ }
+
+ /**
+ * Adds CSS to an existing media/selector
+ * @param string $media
+ * @param string $selector
+ * @param array $css_add
+ * @access private
+ * @version 1.1
+ */
+ function merge_css_blocks($media, $selector, $css_add) {
+ foreach ($css_add as $property => $value) {
+ $this->css_add_property($media, $selector, $property, $value, false);
+ }
+ }
+
+ /**
+ * Checks if $value is !important.
+ * @param string $value
+ * @return bool
+ * @access public
+ * @version 1.0
+ */
+ static function is_important(&$value) {
+ return (!strcasecmp(substr(str_replace($GLOBALS['csstidy']['whitespace'], '', $value), -10, 10), '!important'));
+ }
+
+ /**
+ * Returns a value without !important
+ * @param string $value
+ * @return string
+ * @access public
+ * @version 1.0
+ */
+ static function gvw_important($value) {
+ if (csstidy::is_important($value)) {
+ $value = trim($value);
+ $value = substr($value, 0, -9);
+ $value = trim($value);
+ $value = substr($value, 0, -1);
+ $value = trim($value);
+ return $value;
+ }
+ return $value;
+ }
+
+ /**
+ * Checks if the next word in a string from pos is a CSS property
+ * @param string $istring
+ * @param integer $pos
+ * @return bool
+ * @access private
+ * @version 1.2
+ */
+ function property_is_next($istring, $pos) {
+ $all_properties = & $GLOBALS['csstidy']['all_properties'];
+ $istring = substr($istring, $pos, strlen($istring) - $pos);
+ $pos = strpos($istring, ':');
+ if ($pos === false) {
+ return false;
+ }
+ $istring = strtolower(trim(substr($istring, 0, $pos)));
+ if (isset($all_properties[$istring])) {
+ $this->log('Added semicolon to the end of declaration', 'Warning');
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks if a property is valid
+ * @param string $property
+ * @return bool;
+ * @access public
+ * @version 1.0
+ */
+ function property_is_valid($property) {
+ $property = strtolower($property);
+ if (in_array(trim($property), $GLOBALS['csstidy']['multiple_properties'])) $property = trim($property);
+ $all_properties = & $GLOBALS['csstidy']['all_properties'];
+ return (isset($all_properties[$property]) && strpos($all_properties[$property], strtoupper($this->get_cfg('css_level'))) !== false );
+ }
+
+ /**
+ * Accepts a list of strings (e.g., the argument to format() in a @font-face src property)
+ * and returns a list of the strings. Converts things like:
+ *
+ * format(abc) => format("abc")
+ * format(abc def) => format("abc","def")
+ * format(abc "def") => format("abc","def")
+ * format(abc, def, ghi) => format("abc","def","ghi")
+ * format("abc",'def') => format("abc","def")
+ * format("abc, def, ghi") => format("abc, def, ghi")
+ *
+ * @param string
+ * @return array
+ */
+
+ function parse_string_list($value) {
+ $value = trim($value);
+
+ // Case: empty
+ if (!$value) return array();
+
+ $strings = array();
+
+ $in_str = false;
+ $current_string = "";
+
+ for ($i = 0, $_len = strlen($value); $i < $_len; $i++) {
+ if (($value{$i} == "," || $value{$i} === " ") && $in_str === true) {
+ $in_str = false;
+ $strings[] = $current_string;
+ $current_string = "";
+ }
+ else if ($value{$i} == '"' || $value{$i} == "'"){
+ if ($in_str === $value{$i}) {
+ $strings[] = $current_string;
+ $in_str = false;
+ $current_string = "";
+ continue;
+ }
+ else if (!$in_str) {
+ $in_str = $value{$i};
+ }
+ }
+ else {
+ if ($in_str){
+ $current_string .= $value{$i};
+ }
+ else {
+ if (!preg_match("/[\s,]/", $value{$i})) {
+ $in_str = true;
+ $current_string = $value{$i};
+ }
+ }
+ }
+ }
+
+ if ($current_string) {
+ $strings[] = $current_string;
+ }
+
+ return $strings;
+ }
+}
diff --git a/plugins/jetpack/modules/custom-css/csstidy/class.csstidy_ctype.php b/plugins/jetpack/modules/custom-css/csstidy/class.csstidy_ctype.php
new file mode 100644
index 00000000..bc5accc5
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/class.csstidy_ctype.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * CSSTidy - CSS Parser and Optimiser
+ *
+ * CSS ctype functions
+ * Defines some functions that can be not defined.
+ *
+ * This file is part of CSSTidy.
+ *
+ * CSSTidy is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * CSSTidy is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with CSSTidy; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * @license http://opensource.org/licenses/gpl-license.php GNU Public License
+ * @package csstidy
+ * @author Nikolay Matsievsky (speed at webo dot name) 2009-2010
+ * @version 1.0
+ */
+/* ctype_space Check for whitespace character(s) */
+if (!function_exists('ctype_space')) {
+
+ function ctype_space($text) {
+ return!preg_match("/[^\s\r\n\t\f]/", $text);
+ }
+
+}
+/* ctype_alpha Check for alphabetic character(s) */
+if (!function_exists('ctype_alpha')) {
+
+ function ctype_alpha($text) {
+ return preg_match("/[a-zA-Z]/", $text);
+ }
+
+}
+?> \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/csstidy/class.csstidy_optimise.php b/plugins/jetpack/modules/custom-css/csstidy/class.csstidy_optimise.php
new file mode 100644
index 00000000..176e0fd3
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/class.csstidy_optimise.php
@@ -0,0 +1,938 @@
+<?php
+
+/**
+ * CSSTidy - CSS Parser and Optimiser
+ *
+ * CSS Optimising Class
+ * This class optimises CSS data generated by csstidy.
+ *
+ * Copyright 2005, 2006, 2007 Florian Schmitz
+ *
+ * This file is part of CSSTidy.
+ *
+ * CSSTidy is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * CSSTidy is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @license http://opensource.org/licenses/lgpl-license.php GNU Lesser General Public License
+ * @package csstidy
+ * @author Florian Schmitz (floele at gmail dot com) 2005-2007
+ * @author Brett Zamir (brettz9 at yahoo dot com) 2007
+ * @author Nikolay Matsievsky (speed at webo dot name) 2009-2010
+ */
+
+/**
+ * CSS Optimising Class
+ *
+ * This class optimises CSS data generated by csstidy.
+ *
+ * @package csstidy
+ * @author Florian Schmitz (floele at gmail dot com) 2005-2006
+ * @version 1.0
+ */
+class csstidy_optimise {
+ /**
+ * Constructor
+ * @param array $css contains the class csstidy
+ * @access private
+ * @version 1.0
+ */
+ function __construct(&$css) {
+ $this->parser = & $css;
+ $this->css = & $css->css;
+ $this->sub_value = & $css->sub_value;
+ $this->at = & $css->at;
+ $this->selector = & $css->selector;
+ $this->property = & $css->property;
+ $this->value = & $css->value;
+ }
+
+ function csstidy_optimise(&$css) {
+ $this->__construct($css);
+ }
+
+ /**
+ * Optimises $css after parsing
+ * @access public
+ * @version 1.0
+ */
+ function postparse() {
+ if ($this->parser->get_cfg('preserve_css')) {
+ return;
+ }
+
+ if ($this->parser->get_cfg('merge_selectors') === 2) {
+ foreach ($this->css as $medium => $value) {
+ $this->merge_selectors($this->css[$medium]);
+ }
+ }
+
+ if ($this->parser->get_cfg('discard_invalid_selectors')) {
+ foreach ($this->css as $medium => $value) {
+ $this->discard_invalid_selectors($this->css[$medium]);
+ }
+ }
+
+ if ($this->parser->get_cfg('optimise_shorthands') > 0) {
+ foreach ($this->css as $medium => $value) {
+ foreach ($value as $selector => $value1) {
+ $this->css[$medium][$selector] = csstidy_optimise::merge_4value_shorthands($this->css[$medium][$selector]);
+
+ if ($this->parser->get_cfg('optimise_shorthands') < 2) {
+ continue;
+ }
+
+ $this->css[$medium][$selector] = csstidy_optimise::merge_font($this->css[$medium][$selector]);
+
+ if ($this->parser->get_cfg('optimise_shorthands') < 3) {
+ continue;
+ }
+
+ $this->css[$medium][$selector] = csstidy_optimise::merge_bg($this->css[$medium][$selector]);
+ if (empty($this->css[$medium][$selector])) {
+ unset($this->css[$medium][$selector]);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Optimises values
+ * @access public
+ * @version 1.0
+ */
+ function value() {
+ $shorthands = & $GLOBALS['csstidy']['shorthands'];
+
+ // optimise shorthand properties
+ if (isset($shorthands[$this->property])) {
+ $temp = csstidy_optimise::shorthand($this->value); // FIXME - move
+ if ($temp != $this->value) {
+ $this->parser->log('Optimised shorthand notation (' . $this->property . '): Changed "' . $this->value . '" to "' . $temp . '"', 'Information');
+ }
+ $this->value = $temp;
+ }
+
+ // Remove whitespace at ! important
+ if ($this->value != $this->compress_important($this->value)) {
+ $this->parser->log('Optimised !important', 'Information');
+ }
+ }
+
+ /**
+ * Optimises shorthands
+ * @access public
+ * @version 1.0
+ */
+ function shorthands() {
+ $shorthands = & $GLOBALS['csstidy']['shorthands'];
+
+ if (!$this->parser->get_cfg('optimise_shorthands') || $this->parser->get_cfg('preserve_css')) {
+ return;
+ }
+
+ if ($this->property === 'font' && $this->parser->get_cfg('optimise_shorthands') > 1) {
+ $this->css[$this->at][$this->selector]['font']='';
+ $this->parser->merge_css_blocks($this->at, $this->selector, csstidy_optimise::dissolve_short_font($this->value));
+ }
+ if ($this->property === 'background' && $this->parser->get_cfg('optimise_shorthands') > 2) {
+ $this->css[$this->at][$this->selector]['background']='';
+ $this->parser->merge_css_blocks($this->at, $this->selector, csstidy_optimise::dissolve_short_bg($this->value));
+ }
+ if (isset($shorthands[$this->property])) {
+ $this->parser->merge_css_blocks($this->at, $this->selector, csstidy_optimise::dissolve_4value_shorthands($this->property, $this->value));
+ if (is_array($shorthands[$this->property])) {
+ $this->css[$this->at][$this->selector][$this->property] = '';
+ }
+ }
+ }
+
+ /**
+ * Optimises a sub-value
+ * @access public
+ * @version 1.0
+ */
+ function subvalue() {
+ $replace_colors = & $GLOBALS['csstidy']['replace_colors'];
+
+ $this->sub_value = trim($this->sub_value);
+ if ($this->sub_value == '') { // caution : '0'
+ return;
+ }
+
+ $important = '';
+ if (csstidy::is_important($this->sub_value)) {
+ $important = '!important';
+ }
+ $this->sub_value = csstidy::gvw_important($this->sub_value);
+
+ // Compress font-weight
+ if ($this->property === 'font-weight' && $this->parser->get_cfg('compress_font-weight')) {
+ if ($this->sub_value === 'bold') {
+ $this->sub_value = '700';
+ $this->parser->log('Optimised font-weight: Changed "bold" to "700"', 'Information');
+ } else if ($this->sub_value === 'normal') {
+ $this->sub_value = '400';
+ $this->parser->log('Optimised font-weight: Changed "normal" to "400"', 'Information');
+ }
+ }
+
+ $temp = $this->compress_numbers($this->sub_value);
+ if (strcasecmp($temp, $this->sub_value) !== 0) {
+ if (strlen($temp) > strlen($this->sub_value)) {
+ $this->parser->log('Fixed invalid number: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Warning');
+ } else {
+ $this->parser->log('Optimised number: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Information');
+ }
+ $this->sub_value = $temp;
+ }
+ if ($this->parser->get_cfg('compress_colors')) {
+ $temp = $this->cut_color($this->sub_value);
+ if ($temp !== $this->sub_value) {
+ if (isset($replace_colors[$this->sub_value])) {
+ $this->parser->log('Fixed invalid color name: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Warning');
+ } else {
+ $this->parser->log('Optimised color: Changed "' . $this->sub_value . '" to "' . $temp . '"', 'Information');
+ }
+ $this->sub_value = $temp;
+ }
+ }
+ $this->sub_value .= $important;
+ }
+
+ /**
+ * Compresses shorthand values. Example: margin:1px 1px 1px 1px -> margin:1px
+ * @param string $value
+ * @access public
+ * @return string
+ * @version 1.0
+ */
+ static function shorthand($value) {
+ $important = '';
+ if (csstidy::is_important($value)) {
+ $values = csstidy::gvw_important($value);
+ $important = '!important';
+ }
+ else
+ $values = $value;
+
+ $values = explode(' ', $values);
+ switch (count($values)) {
+ case 4:
+ if ($values[0] == $values[1] && $values[0] == $values[2] && $values[0] == $values[3]) {
+ return $values[0] . $important;
+ } elseif ($values[1] == $values[3] && $values[0] == $values[2]) {
+ return $values[0] . ' ' . $values[1] . $important;
+ } elseif ($values[1] == $values[3]) {
+ return $values[0] . ' ' . $values[1] . ' ' . $values[2] . $important;
+ }
+ break;
+
+ case 3:
+ if ($values[0] == $values[1] && $values[0] == $values[2]) {
+ return $values[0] . $important;
+ } elseif ($values[0] == $values[2]) {
+ return $values[0] . ' ' . $values[1] . $important;
+ }
+ break;
+
+ case 2:
+ if ($values[0] == $values[1]) {
+ return $values[0] . $important;
+ }
+ break;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Removes unnecessary whitespace in ! important
+ * @param string $string
+ * @return string
+ * @access public
+ * @version 1.1
+ */
+ function compress_important(&$string) {
+ if (csstidy::is_important($string)) {
+ $string = csstidy::gvw_important($string) . ' !important'; }
+ return $string;
+ }
+
+ /**
+ * Color compression function. Converts all rgb() values to #-values and uses the short-form if possible. Also replaces 4 color names by #-values.
+ * @param string $color
+ * @return string
+ * @version 1.1
+ */
+ function cut_color($color) {
+ $replace_colors = & $GLOBALS['csstidy']['replace_colors'];
+
+ // rgb(0,0,0) -> #000000 (or #000 in this case later)
+ if (strtolower(substr($color, 0, 4)) === 'rgb(') {
+ $color_tmp = substr($color, 4, strlen($color) - 5);
+ $color_tmp = explode(',', $color_tmp);
+ for ($i = 0; $i < count($color_tmp); $i++) {
+ $color_tmp[$i] = trim($color_tmp[$i]);
+ if (substr($color_tmp[$i], -1) === '%') {
+ $color_tmp[$i] = round((255 * $color_tmp[$i]) / 100);
+ }
+ if ($color_tmp[$i] > 255)
+ $color_tmp[$i] = 255;
+ }
+ $color = '#';
+ for ($i = 0; $i < 3; $i++) {
+ if ($color_tmp[$i] < 16) {
+ $color .= '0' . dechex($color_tmp[$i]);
+ } else {
+ $color .= dechex($color_tmp[$i]);
+ }
+ }
+ }
+
+ // Fix bad color names
+ if (isset($replace_colors[strtolower($color)])) {
+ $color = $replace_colors[strtolower($color)];
+ }
+
+ // #aabbcc -> #abc
+ if (strlen($color) == 7) {
+ $color_temp = strtolower($color);
+ if ($color_temp{0} === '#' && $color_temp{1} == $color_temp{2} && $color_temp{3} == $color_temp{4} && $color_temp{5} == $color_temp{6}) {
+ $color = '#' . $color{1} . $color{3} . $color{5};
+ }
+ }
+
+ switch (strtolower($color)) {
+ /* color name -> hex code */
+ case 'black': return '#000';
+ case 'fuchsia': return '#f0f';
+ case 'white': return '#fff';
+ case 'yellow': return '#ff0';
+
+ /* hex code -> color name */
+ case '#800000': return 'maroon';
+ case '#ffa500': return 'orange';
+ case '#808000': return 'olive';
+ case '#800080': return 'purple';
+ case '#008000': return 'green';
+ case '#000080': return 'navy';
+ case '#008080': return 'teal';
+ case '#c0c0c0': return 'silver';
+ case '#808080': return 'gray';
+ case '#f00': return 'red';
+ }
+
+ return $color;
+ }
+
+ /**
+ * Compresses numbers (ie. 1.0 becomes 1 or 1.100 becomes 1.1 )
+ * @param string $subvalue
+ * @return string
+ * @version 1.2
+ */
+ function compress_numbers($subvalue) {
+ $unit_values = & $GLOBALS['csstidy']['unit_values'];
+ $color_values = & $GLOBALS['csstidy']['color_values'];
+
+ // for font:1em/1em sans-serif...;
+ if ($this->property === 'font') {
+ $temp = explode('/', $subvalue);
+ } else {
+ $temp = array($subvalue);
+ }
+ for ($l = 0; $l < count($temp); $l++) {
+ // if we are not dealing with a number at this point, do not optimise anything
+ $number = $this->AnalyseCssNumber($temp[$l]);
+ if ($number === false) {
+ return $subvalue;
+ }
+
+ // Fix bad colors
+ if (in_array($this->property, $color_values)) {
+ if (strlen($temp[$l]) == 3 || strlen($temp[$l]) == 6) {
+ $temp[$l] = '#' . $temp[$l];
+ }
+ else {
+ $temp[$l] = "0";
+ }
+ continue;
+ }
+
+ if (abs($number[0]) > 0) {
+ if ($number[1] == '' && in_array($this->property, $unit_values, true)) {
+ $number[1] = 'px';
+ }
+ } else {
+ $number[1] = '';
+ }
+
+ $temp[$l] = $number[0] . $number[1];
+ }
+
+ return ((count($temp) > 1) ? $temp[0] . '/' . $temp[1] : $temp[0]);
+ }
+
+ /**
+ * Checks if a given string is a CSS valid number. If it is,
+ * an array containing the value and unit is returned
+ * @param string $string
+ * @return array ('unit' if unit is found or '' if no unit exists, number value) or false if no number
+ */
+ function AnalyseCssNumber($string) {
+ // most simple checks first
+ if (strlen($string) == 0 || ctype_alpha($string{0})) {
+ return false;
+ }
+
+ $units = & $GLOBALS['csstidy']['units'];
+ $return = array(0, '');
+
+ $return[0] = floatval($string);
+ if (abs($return[0]) > 0 && abs($return[0]) < 1) {
+ if ($return[0] < 0) {
+ $return[0] = '-' . ltrim(substr($return[0], 1), '0');
+ } else {
+ $return[0] = ltrim($return[0], '0');
+ }
+ }
+
+ // Look for unit and split from value if exists
+ foreach ($units as $unit) {
+ $expectUnitAt = strlen($string) - strlen($unit);
+ if (!($unitInString = stristr($string, $unit))) { // mb_strpos() fails with "false"
+ continue;
+ }
+ $actualPosition = strpos($string, $unitInString);
+ if ($expectUnitAt === $actualPosition) {
+ $return[1] = $unit;
+ $string = substr($string, 0, - strlen($unit));
+ break;
+ }
+ }
+ if (!is_numeric($string)) {
+ return false;
+ }
+ return $return;
+ }
+
+ /**
+ * Merges selectors with same properties. Example: a{color:red} b{color:red} -> a,b{color:red}
+ * Very basic and has at least one bug. Hopefully there is a replacement soon.
+ * @param array $array
+ * @return array
+ * @access public
+ * @version 1.2
+ */
+ function merge_selectors(&$array) {
+ $css = $array;
+ foreach ($css as $key => $value) {
+ if (!isset($css[$key])) {
+ continue;
+ }
+ $newsel = '';
+
+ // Check if properties also exist in another selector
+ $keys = array();
+ // PHP bug (?) without $css = $array; here
+ foreach ($css as $selector => $vali) {
+ if ($selector == $key) {
+ continue;
+ }
+
+ if ($css[$key] === $vali) {
+ $keys[] = $selector;
+ }
+ }
+
+ if (!empty($keys)) {
+ $newsel = $key;
+ unset($css[$key]);
+ foreach ($keys as $selector) {
+ unset($css[$selector]);
+ $newsel .= ',' . $selector;
+ }
+ $css[$newsel] = $value;
+ }
+ }
+ $array = $css;
+ }
+
+ /**
+ * Removes invalid selectors and their corresponding rule-sets as
+ * defined by 4.1.7 in REC-CSS2. This is a very rudimentary check
+ * and should be replaced by a full-blown parsing algorithm or
+ * regular expression
+ * @version 1.4
+ */
+ function discard_invalid_selectors(&$array) {
+ $invalid = array('+' => true, '~' => true, ',' => true, '>' => true);
+ foreach ($array as $selector => $decls) {
+ $ok = true;
+ $selectors = array_map('trim', explode(',', $selector));
+ foreach ($selectors as $s) {
+ $simple_selectors = preg_split('/\s*[+>~\s]\s*/', $s);
+ foreach ($simple_selectors as $ss) {
+ if ($ss === '')
+ $ok = false;
+ // could also check $ss for internal structure,
+ // but that probably would be too slow
+ }
+ }
+ if (!$ok)
+ unset($array[$selector]);
+ }
+ }
+
+ /**
+ * Dissolves properties like padding:10px 10px 10px to padding-top:10px;padding-bottom:10px;...
+ * @param string $property
+ * @param string $value
+ * @return array
+ * @version 1.0
+ * @see merge_4value_shorthands()
+ */
+ static function dissolve_4value_shorthands($property, $value) {
+ $shorthands = & $GLOBALS['csstidy']['shorthands'];
+ if (!is_array($shorthands[$property])) {
+ $return[$property] = $value;
+ return $return;
+ }
+
+ $important = '';
+ if (csstidy::is_important($value)) {
+ $value = csstidy::gvw_important($value);
+ $important = '!important';
+ }
+ $values = explode(' ', $value);
+
+
+ $return = array();
+ if (count($values) == 4) {
+ for ($i = 0; $i < 4; $i++) {
+ $return[$shorthands[$property][$i]] = $values[$i] . $important;
+ }
+ } elseif (count($values) == 3) {
+ $return[$shorthands[$property][0]] = $values[0] . $important;
+ $return[$shorthands[$property][1]] = $values[1] . $important;
+ $return[$shorthands[$property][3]] = $values[1] . $important;
+ $return[$shorthands[$property][2]] = $values[2] . $important;
+ } elseif (count($values) == 2) {
+ for ($i = 0; $i < 4; $i++) {
+ $return[$shorthands[$property][$i]] = (($i % 2 != 0)) ? $values[1] . $important : $values[0] . $important;
+ }
+ } else {
+ for ($i = 0; $i < 4; $i++) {
+ $return[$shorthands[$property][$i]] = $values[0] . $important;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Explodes a string as explode() does, however, not if $sep is escaped or within a string.
+ * @param string $sep seperator
+ * @param string $string
+ * @return array
+ * @version 1.0
+ */
+ static function explode_ws($sep, $string) {
+ $status = 'st';
+ $to = '';
+
+ $output = array();
+ $num = 0;
+ for ($i = 0, $len = strlen($string); $i < $len; $i++) {
+ switch ($status) {
+ case 'st':
+ if ($string{$i} == $sep && !csstidy::escaped($string, $i)) {
+ ++$num;
+ } elseif ($string{$i} === '"' || $string{$i} === '\'' || $string{$i} === '(' && !csstidy::escaped($string, $i)) {
+ $status = 'str';
+ $to = ($string{$i} === '(') ? ')' : $string{$i};
+ (isset($output[$num])) ? $output[$num] .= $string{$i} : $output[$num] = $string{$i};
+ } else {
+ (isset($output[$num])) ? $output[$num] .= $string{$i} : $output[$num] = $string{$i};
+ }
+ break;
+
+ case 'str':
+ if ($string{$i} == $to && !csstidy::escaped($string, $i)) {
+ $status = 'st';
+ }
+ (isset($output[$num])) ? $output[$num] .= $string{$i} : $output[$num] = $string{$i};
+ break;
+ }
+ }
+
+ if (isset($output[0])) {
+ return $output;
+ } else {
+ return array($output);
+ }
+ }
+
+ /**
+ * Merges Shorthand properties again, the opposite of dissolve_4value_shorthands()
+ * @param array $array
+ * @return array
+ * @version 1.2
+ * @see dissolve_4value_shorthands()
+ */
+ static function merge_4value_shorthands($array) {
+ $return = $array;
+ $shorthands = & $GLOBALS['csstidy']['shorthands'];
+
+ foreach ($shorthands as $key => $value) {
+ if (isset($array[$value[0]]) && isset($array[$value[1]])
+ && isset($array[$value[2]]) && isset($array[$value[3]]) && $value !== 0) {
+ $return[$key] = '';
+
+ $important = '';
+ for ($i = 0; $i < 4; $i++) {
+ $val = $array[$value[$i]];
+ if (csstidy::is_important($val)) {
+ $important = '!important';
+ $return[$key] .= csstidy::gvw_important($val) . ' ';
+ } else {
+ $return[$key] .= $val . ' ';
+ }
+ unset($return[$value[$i]]);
+ }
+ $return[$key] = csstidy_optimise::shorthand(trim($return[$key] . $important));
+ }
+ }
+ return $return;
+ }
+
+ /**
+ * Dissolve background property
+ * @param string $str_value
+ * @return array
+ * @version 1.0
+ * @see merge_bg()
+ * @todo full CSS 3 compliance
+ */
+ static function dissolve_short_bg($str_value) {
+ // don't try to explose background gradient !
+ if (stripos($str_value, "gradient(")!==FALSE)
+ return array('background'=>$str_value);
+
+ $background_prop_default = & $GLOBALS['csstidy']['background_prop_default'];
+ $repeat = array('repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'space');
+ $attachment = array('scroll', 'fixed', 'local');
+ $clip = array('border', 'padding');
+ $origin = array('border', 'padding', 'content');
+ $pos = array('top', 'center', 'bottom', 'left', 'right');
+ $important = '';
+ $return = array('background-image' => null, 'background-size' => null, 'background-repeat' => null, 'background-position' => null, 'background-attachment' => null, 'background-clip' => null, 'background-origin' => null, 'background-color' => null);
+
+ if (csstidy::is_important($str_value)) {
+ $important = ' !important';
+ $str_value = csstidy::gvw_important($str_value);
+ }
+
+ $str_value = csstidy_optimise::explode_ws(',', $str_value);
+ for ($i = 0; $i < count($str_value); $i++) {
+ $have['clip'] = false;
+ $have['pos'] = false;
+ $have['color'] = false;
+ $have['bg'] = false;
+
+ if (is_array($str_value[$i])) {
+ $str_value[$i] = $str_value[$i][0];
+ }
+ $str_value[$i] = csstidy_optimise::explode_ws(' ', trim($str_value[$i]));
+
+ for ($j = 0; $j < count($str_value[$i]); $j++) {
+ if ($have['bg'] === false && (substr($str_value[$i][$j], 0, 4) === 'url(' || $str_value[$i][$j] === 'none')) {
+ $return['background-image'] .= $str_value[$i][$j] . ',';
+ $have['bg'] = true;
+ } elseif (in_array($str_value[$i][$j], $repeat, true)) {
+ $return['background-repeat'] .= $str_value[$i][$j] . ',';
+ } elseif (in_array($str_value[$i][$j], $attachment, true)) {
+ $return['background-attachment'] .= $str_value[$i][$j] . ',';
+ } elseif (in_array($str_value[$i][$j], $clip, true) && !$have['clip']) {
+ $return['background-clip'] .= $str_value[$i][$j] . ',';
+ $have['clip'] = true;
+ } elseif (in_array($str_value[$i][$j], $origin, true)) {
+ $return['background-origin'] .= $str_value[$i][$j] . ',';
+ } elseif ($str_value[$i][$j]{0} === '(') {
+ $return['background-size'] .= substr($str_value[$i][$j], 1, -1) . ',';
+ } elseif (in_array($str_value[$i][$j], $pos, true) || is_numeric($str_value[$i][$j]{0}) || $str_value[$i][$j]{0} === null || $str_value[$i][$j]{0} === '-' || $str_value[$i][$j]{0} === '.') {
+ $return['background-position'] .= $str_value[$i][$j];
+ if (!$have['pos'])
+ $return['background-position'] .= ' '; else
+ $return['background-position'].= ',';
+ $have['pos'] = true;
+ }
+ elseif (!$have['color']) {
+ $return['background-color'] .= $str_value[$i][$j] . ',';
+ $have['color'] = true;
+ }
+ }
+ }
+
+ foreach ($background_prop_default as $bg_prop => $default_value) {
+ if ($return[$bg_prop] !== null) {
+ $return[$bg_prop] = substr($return[$bg_prop], 0, -1) . $important;
+ }
+ else
+ $return[$bg_prop] = $default_value . $important;
+ }
+ return $return;
+ }
+
+ /**
+ * Merges all background properties
+ * @param array $input_css
+ * @return array
+ * @version 1.0
+ * @see dissolve_short_bg()
+ * @todo full CSS 3 compliance
+ */
+ static function merge_bg($input_css) {
+ $background_prop_default = & $GLOBALS['csstidy']['background_prop_default'];
+ // Max number of background images. CSS3 not yet fully implemented
+ $number_of_values = @max(count(csstidy_optimise::explode_ws(',', $input_css['background-image'])), count(csstidy_optimise::explode_ws(',', $input_css['background-color'])), 1);
+ // Array with background images to check if BG image exists
+ $bg_img_array = @csstidy_optimise::explode_ws(',', csstidy::gvw_important($input_css['background-image']));
+ $new_bg_value = '';
+ $important = '';
+
+ // if background properties is here and not empty, don't try anything
+ if (isset($input_css['background']) AND $input_css['background'])
+ return $input_css;
+
+ for ($i = 0; $i < $number_of_values; $i++) {
+ foreach ($background_prop_default as $bg_property => $default_value) {
+ // Skip if property does not exist
+ if (!isset($input_css[$bg_property])) {
+ continue;
+ }
+
+ $cur_value = $input_css[$bg_property];
+ // skip all optimisation if gradient() somewhere
+ if (stripos($cur_value, "gradient(")!==FALSE)
+ return $input_css;
+
+ // Skip some properties if there is no background image
+ if ((!isset($bg_img_array[$i]) || $bg_img_array[$i] === 'none')
+ && ($bg_property === 'background-size' || $bg_property === 'background-position'
+ || $bg_property === 'background-attachment' || $bg_property === 'background-repeat')) {
+ continue;
+ }
+
+ // Remove !important
+ if (csstidy::is_important($cur_value)) {
+ $important = ' !important';
+ $cur_value = csstidy::gvw_important($cur_value);
+ }
+
+ // Do not add default values
+ if ($cur_value === $default_value) {
+ continue;
+ }
+
+ $temp = csstidy_optimise::explode_ws(',', $cur_value);
+
+ if (isset($temp[$i])) {
+ if ($bg_property === 'background-size') {
+ $new_bg_value .= '(' . $temp[$i] . ') ';
+ } else {
+ $new_bg_value .= $temp[$i] . ' ';
+ }
+ }
+ }
+
+ $new_bg_value = trim($new_bg_value);
+ if ($i != $number_of_values - 1)
+ $new_bg_value .= ',';
+ }
+
+ // Delete all background-properties
+ foreach ($background_prop_default as $bg_property => $default_value) {
+ unset($input_css[$bg_property]);
+ }
+
+ // Add new background property
+ if ($new_bg_value !== '')
+ $input_css['background'] = $new_bg_value . $important;
+ elseif(isset ($input_css['background']))
+ $input_css['background'] = 'none';
+
+ return $input_css;
+ }
+
+ /**
+ * Dissolve font property
+ * @param string $str_value
+ * @return array
+ * @version 1.3
+ * @see merge_font()
+ */
+ static function dissolve_short_font($str_value) {
+ $font_prop_default = & $GLOBALS['csstidy']['font_prop_default'];
+ $font_weight = array('normal', 'bold', 'bolder', 'lighter', 100, 200, 300, 400, 500, 600, 700, 800, 900);
+ $font_variant = array('normal', 'small-caps');
+ $font_style = array('normal', 'italic', 'oblique');
+ $important = '';
+ $return = array('font-style' => null, 'font-variant' => null, 'font-weight' => null, 'font-size' => null, 'line-height' => null, 'font-family' => null);
+
+ if (csstidy::is_important($str_value)) {
+ $important = '!important';
+ $str_value = csstidy::gvw_important($str_value);
+ }
+
+ $have['style'] = false;
+ $have['variant'] = false;
+ $have['weight'] = false;
+ $have['size'] = false;
+ // Detects if font-family consists of several words w/o quotes
+ $multiwords = false;
+
+ // Workaround with multiple font-family
+ $str_value = csstidy_optimise::explode_ws(',', trim($str_value));
+
+ $str_value[0] = csstidy_optimise::explode_ws(' ', trim($str_value[0]));
+
+ for ($j = 0; $j < count($str_value[0]); $j++) {
+ if ($have['weight'] === false && in_array($str_value[0][$j], $font_weight)) {
+ $return['font-weight'] = $str_value[0][$j];
+ $have['weight'] = true;
+ } elseif ($have['variant'] === false && in_array($str_value[0][$j], $font_variant)) {
+ $return['font-variant'] = $str_value[0][$j];
+ $have['variant'] = true;
+ } elseif ($have['style'] === false && in_array($str_value[0][$j], $font_style)) {
+ $return['font-style'] = $str_value[0][$j];
+ $have['style'] = true;
+ } elseif ($have['size'] === false && (is_numeric($str_value[0][$j]{0}) || $str_value[0][$j]{0} === null || $str_value[0][$j]{0} === '.')) {
+ $size = csstidy_optimise::explode_ws('/', trim($str_value[0][$j]));
+ $return['font-size'] = $size[0];
+ if (isset($size[1])) {
+ $return['line-height'] = $size[1];
+ } else {
+ $return['line-height'] = ''; // don't add 'normal' !
+ }
+ $have['size'] = true;
+ } else {
+ if (isset($return['font-family'])) {
+ $return['font-family'] .= ' ' . $str_value[0][$j];
+ $multiwords = true;
+ } else {
+ $return['font-family'] = $str_value[0][$j];
+ }
+ }
+ }
+ // add quotes if we have several qords in font-family
+ if ($multiwords !== false) {
+ $return['font-family'] = '"' . $return['font-family'] . '"';
+ }
+ $i = 1;
+ while (isset($str_value[$i])) {
+ $return['font-family'] .= ',' . trim($str_value[$i]);
+ $i++;
+ }
+
+ // Fix for 100 and more font-size
+ if ($have['size'] === false && isset($return['font-weight']) &&
+ is_numeric($return['font-weight']{0})) {
+ $return['font-size'] = $return['font-weight'];
+ unset($return['font-weight']);
+ }
+
+ foreach ($font_prop_default as $font_prop => $default_value) {
+ if ($return[$font_prop] !== null) {
+ $return[$font_prop] = $return[$font_prop] . $important;
+ }
+ else
+ $return[$font_prop] = $default_value . $important;
+ }
+ return $return;
+ }
+
+ /**
+ * Merges all fonts properties
+ * @param array $input_css
+ * @return array
+ * @version 1.3
+ * @see dissolve_short_font()
+ */
+ static function merge_font($input_css) {
+ $font_prop_default = & $GLOBALS['csstidy']['font_prop_default'];
+ $new_font_value = '';
+ $important = '';
+ // Skip if not font-family and font-size set
+ if (isset($input_css['font-family']) && isset($input_css['font-size'])) {
+ // fix several words in font-family - add quotes
+ if (isset($input_css['font-family'])) {
+ $families = explode(",", $input_css['font-family']);
+ $result_families = array();
+ foreach ($families as $family) {
+ $family = trim($family);
+ $len = strlen($family);
+ if (strpos($family, " ") &&
+ !(($family{0} == '"' && $family{$len - 1} == '"') ||
+ ($family{0} == "'" && $family{$len - 1} == "'"))) {
+ $family = '"' . $family . '"';
+ }
+ $result_families[] = $family;
+ }
+ $input_css['font-family'] = implode(",", $result_families);
+ }
+ foreach ($font_prop_default as $font_property => $default_value) {
+
+ // Skip if property does not exist
+ if (!isset($input_css[$font_property])) {
+ continue;
+ }
+
+ $cur_value = $input_css[$font_property];
+
+ // Skip if default value is used
+ if ($cur_value === $default_value) {
+ continue;
+ }
+
+ // Remove !important
+ if (csstidy::is_important($cur_value)) {
+ $important = '!important';
+ $cur_value = csstidy::gvw_important($cur_value);
+ }
+
+ $new_font_value .= $cur_value;
+ // Add delimiter
+ $new_font_value .= ( $font_property === 'font-size' &&
+ isset($input_css['line-height'])) ? '/' : ' ';
+ }
+
+ $new_font_value = trim($new_font_value);
+
+ // Delete all font-properties
+ foreach ($font_prop_default as $font_property => $default_value) {
+ if ($font_property!=='font' OR !$new_font_value)
+ unset($input_css[$font_property]);
+ }
+
+ // Add new font property
+ if ($new_font_value !== '') {
+ $input_css['font'] = $new_font_value . $important;
+ }
+ }
+
+ return $input_css;
+ }
+
+}
diff --git a/plugins/jetpack/modules/custom-css/csstidy/class.csstidy_print.php b/plugins/jetpack/modules/custom-css/csstidy/class.csstidy_print.php
new file mode 100644
index 00000000..56b95404
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/class.csstidy_print.php
@@ -0,0 +1,410 @@
+<?php
+/**
+ * CSSTidy - CSS Parser and Optimiser
+ *
+ * CSS Printing class
+ * This class prints CSS data generated by csstidy.
+ *
+ * Copyright 2005, 2006, 2007 Florian Schmitz
+ *
+ * This file is part of CSSTidy.
+ *
+ * CSSTidy is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * CSSTidy is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @license http://opensource.org/licenses/lgpl-license.php GNU Lesser General Public License
+ * @package csstidy
+ * @author Florian Schmitz (floele at gmail dot com) 2005-2007
+ * @author Brett Zamir (brettz9 at yahoo dot com) 2007
+ * @author Cedric Morin (cedric at yterium dot com) 2010
+ */
+
+/**
+ * CSS Printing class
+ *
+ * This class prints CSS data generated by csstidy.
+ *
+ * @package csstidy
+ * @author Florian Schmitz (floele at gmail dot com) 2005-2006
+ * @version 1.0.1
+ */
+class csstidy_print {
+
+ /**
+ * Saves the input CSS string
+ * @var string
+ * @access private
+ */
+ public $input_css = '';
+ /**
+ * Saves the formatted CSS string
+ * @var string
+ * @access public
+ */
+ public $output_css = '';
+ /**
+ * Saves the formatted CSS string (plain text)
+ * @var string
+ * @access public
+ */
+ public $output_css_plain = '';
+
+ /**
+ * Constructor
+ * @param array $css contains the class csstidy
+ * @access private
+ * @version 1.0
+ */
+ function __construct(&$css) {
+ $this->parser = & $css;
+ $this->css = & $css->css;
+ $this->template = & $css->template;
+ $this->tokens = & $css->tokens;
+ $this->charset = & $css->charset;
+ $this->import = & $css->import;
+ $this->namespace = & $css->namespace;
+ }
+
+ function csstidy_print(&$css) {
+ $this->__construct($css);
+ }
+
+ /**
+ * Resets output_css and output_css_plain (new css code)
+ * @access private
+ * @version 1.0
+ */
+ function _reset() {
+ $this->output_css = '';
+ $this->output_css_plain = '';
+ }
+
+ /**
+ * Returns the CSS code as plain text
+ * @param string $default_media default @media to add to selectors without any @media
+ * @return string
+ * @access public
+ * @version 1.0
+ */
+ function plain($default_media='') {
+ $this->_print(true, $default_media);
+ return $this->output_css_plain;
+ }
+
+ /**
+ * Returns the formatted CSS code
+ * @param string $default_media default @media to add to selectors without any @media
+ * @return string
+ * @access public
+ * @version 1.0
+ */
+ function formatted($default_media='') {
+ $this->_print(false, $default_media);
+ return $this->output_css;
+ }
+
+ /**
+ * Returns the formatted CSS code to make a complete webpage
+ * @param string $doctype shorthand for the document type
+ * @param bool $externalcss indicates whether styles to be attached internally or as an external stylesheet
+ * @param string $title title to be added in the head of the document
+ * @param string $lang two-letter language code to be added to the output
+ * @return string
+ * @access public
+ * @version 1.4
+ */
+ function formatted_page($doctype='xhtml1.1', $externalcss=true, $title='', $lang='en') {
+ switch ($doctype) {
+ case 'xhtml1.0strict':
+ $doctype_output = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
+ break;
+ case 'xhtml1.1':
+ default:
+ $doctype_output = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">';
+ break;
+ }
+
+ $output = $cssparsed = '';
+ $this->output_css_plain = & $output;
+
+ $output .= $doctype_output . "\n" . '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="' . $lang . '"';
+ $output .= ( $doctype === 'xhtml1.1') ? '>' : ' lang="' . $lang . '">';
+ $output .= "\n<head>\n <title>$title</title>";
+
+ if ($externalcss) {
+ $output .= "\n <style type=\"text/css\">\n";
+ $cssparsed = file_get_contents('cssparsed.css');
+ $output .= $cssparsed; // Adds an invisible BOM or something, but not in css_optimised.php
+ $output .= "\n</style>";
+ } else {
+ $output .= "\n" . ' <link rel="stylesheet" type="text/css" href="cssparsed.css" />';
+// }
+ }
+ $output .= "\n</head>\n<body><code id=\"copytext\">";
+ $output .= $this->formatted();
+ $output .= '</code>' . "\n" . '</body></html>';
+ return $this->output_css_plain;
+ }
+
+ /**
+ * Returns the formatted CSS Code and saves it into $this->output_css and $this->output_css_plain
+ * @param bool $plain plain text or not
+ * @param string $default_media default @media to add to selectors without any @media
+ * @access private
+ * @version 2.0
+ */
+ function _print($plain = false, $default_media='') {
+ if ($this->output_css && $this->output_css_plain) {
+ return;
+ }
+
+ $output = '';
+ if (!$this->parser->get_cfg('preserve_css')) {
+ $this->_convert_raw_css($default_media);
+ }
+
+ $template = & $this->template;
+
+ if ($plain) {
+ $template = array_map('strip_tags', $template);
+ }
+
+ if ($this->parser->get_cfg('timestamp')) {
+ array_unshift($this->tokens, array(COMMENT, ' CSSTidy ' . $this->parser->version . ': ' . date('r') . ' '));
+ }
+
+ if (!empty($this->charset)) {
+ $output .= $template[0] . '@charset ' . $template[5] . $this->charset . $template[6];
+ }
+
+ if (!empty($this->import)) {
+ for ($i = 0, $size = count($this->import); $i < $size; $i++) {
+ $import_components = explode(' ', $this->import[$i]);
+ if (substr($import_components[0], 0, 4) === 'url(' && substr($import_components[0], -1, 1) === ')') {
+ $import_components[0] = '\'' . trim(substr($import_components[0], 4, -1), "'\"") . '\'';
+ $this->import[$i] = implode(' ', $import_components);
+ $this->parser->log('Optimised @import : Removed "url("', 'Information');
+ }
+ $output .= $template[0] . '@import ' . $template[5] . $this->import[$i] . $template[6];
+ }
+ }
+ if (!empty($this->namespace)) {
+ if (substr($this->namespace, 0, 4) === 'url(' && substr($this->namespace, -1, 1) === ')') {
+ $this->namespace = '\'' . substr($this->namespace, 4, -1) . '\'';
+ $this->parser->log('Optimised @namespace : Removed "url("', 'Information');
+ }
+ $output .= $template[0] . '@namespace ' . $template[5] . $this->namespace . $template[6];
+ }
+
+ $output .= $template[13];
+ $in_at_out = '';
+ $out = & $output;
+
+ foreach ($this->tokens as $key => $token) {
+ switch ($token[0]) {
+ case AT_START:
+ $out .= $template[0] . $this->_htmlsp($token[1], $plain) . $template[1];
+ $out = & $in_at_out;
+ break;
+
+ case SEL_START:
+ if ($this->parser->get_cfg('lowercase_s'))
+ $token[1] = strtolower($token[1]);
+ $out .= ( $token[1]{0} !== '@') ? $template[2] . $this->_htmlsp($token[1], $plain) : $template[0] . $this->_htmlsp($token[1], $plain);
+ $out .= $template[3];
+ break;
+
+ case PROPERTY:
+ if ($this->parser->get_cfg('case_properties') === 2) {
+ $token[1] = strtoupper($token[1]);
+ } elseif ($this->parser->get_cfg('case_properties') === 1) {
+ $token[1] = strtolower($token[1]);
+ }
+ $out .= $template[4] . $this->_htmlsp($token[1], $plain) . ':' . $template[5];
+ break;
+
+ case VALUE:
+ $out .= $this->_htmlsp($token[1], $plain);
+ if ($this->_seeknocomment($key, 1) == SEL_END && $this->parser->get_cfg('remove_last_;')) {
+ $out .= str_replace(';', '', $template[6]);
+ } else {
+ $out .= $template[6];
+ }
+ break;
+
+ case SEL_END:
+ $out .= $template[7];
+ if ($this->_seeknocomment($key, 1) != AT_END)
+ $out .= $template[8];
+ break;
+
+ case AT_END:
+ $out = & $output;
+ $out .= $template[10] . str_replace("\n", "\n" . $template[10], $in_at_out);
+ $in_at_out = '';
+ $out .= $template[9];
+ break;
+
+ case COMMENT:
+ $out .= $template[11] . '/*' . $this->_htmlsp($token[1], $plain) . '*/' . $template[12];
+ break;
+ }
+ }
+
+ $output = trim($output);
+
+ if (!$plain) {
+ $this->output_css = $output;
+ $this->_print(true);
+ } else {
+ // If using spaces in the template, don't want these to appear in the plain output
+ $this->output_css_plain = str_replace('&#160;', '', $output);
+ }
+ }
+
+ /**
+ * Gets the next token type which is $move away from $key, excluding comments
+ * @param integer $key current position
+ * @param integer $move move this far
+ * @return mixed a token type
+ * @access private
+ * @version 1.0
+ */
+ function _seeknocomment($key, $move) {
+ $go = ($move > 0) ? 1 : -1;
+ for ($i = $key + 1; abs($key - $i) - 1 < abs($move); $i += $go) {
+ if (!isset($this->tokens[$i])) {
+ return;
+ }
+ if ($this->tokens[$i][0] == COMMENT) {
+ $move += 1;
+ continue;
+ }
+ return $this->tokens[$i][0];
+ }
+ }
+
+ /**
+ * Converts $this->css array to a raw array ($this->tokens)
+ * @param string $default_media default @media to add to selectors without any @media
+ * @access private
+ * @version 1.0
+ */
+ function _convert_raw_css($default_media='') {
+ $this->tokens = array();
+
+ foreach ($this->css as $medium => $val) {
+ if ($this->parser->get_cfg('sort_selectors'))
+ ksort($val);
+ if (intval($medium) < DEFAULT_AT) {
+ $this->parser->_add_token(AT_START, $medium, true);
+ }
+ elseif ($default_media) {
+ $this->parser->_add_token(AT_START, $default_media, true);
+ }
+
+ foreach ($val as $selector => $vali) {
+ if ($this->parser->get_cfg('sort_properties'))
+ ksort($vali);
+ $this->parser->_add_token(SEL_START, $selector, true);
+
+ foreach ($vali as $property => $valj) {
+ $this->parser->_add_token(PROPERTY, $property, true);
+ $this->parser->_add_token(VALUE, $valj, true);
+ }
+
+ $this->parser->_add_token(SEL_END, $selector, true);
+ }
+
+ if (intval($medium) < DEFAULT_AT) {
+ $this->parser->_add_token(AT_END, $medium, true);
+ }
+ elseif ($default_media) {
+ $this->parser->_add_token(AT_END, $default_media, true);
+ }
+ }
+ }
+
+ /**
+ * Same as htmlspecialchars, only that chars are not replaced if $plain !== true. This makes print_code() cleaner.
+ * @param string $string
+ * @param bool $plain
+ * @return string
+ * @see csstidy_print::_print()
+ * @access private
+ * @version 1.0
+ */
+ function _htmlsp($string, $plain) {
+ if (!$plain) {
+ return htmlspecialchars($string, ENT_QUOTES, 'utf-8');
+ }
+ return $string;
+ }
+
+ /**
+ * Get compression ratio
+ * @access public
+ * @return float
+ * @version 1.2
+ */
+ function get_ratio() {
+ if (!$this->output_css_plain) {
+ $this->formatted();
+ }
+ return round((strlen($this->input_css) - strlen($this->output_css_plain)) / strlen($this->input_css), 3) * 100;
+ }
+
+ /**
+ * Get difference between the old and new code in bytes and prints the code if necessary.
+ * @access public
+ * @return string
+ * @version 1.1
+ */
+ function get_diff() {
+ if (!$this->output_css_plain) {
+ $this->formatted();
+ }
+
+ $diff = strlen($this->output_css_plain) - strlen($this->input_css);
+
+ if ($diff > 0) {
+ return '+' . $diff;
+ } elseif ($diff == 0) {
+ return '+-' . $diff;
+ }
+
+ return $diff;
+ }
+
+ /**
+ * Get the size of either input or output CSS in KB
+ * @param string $loc default is "output"
+ * @access public
+ * @return integer
+ * @version 1.0
+ */
+ function size($loc = 'output') {
+ if ($loc === 'output' && !$this->output_css) {
+ $this->formatted();
+ }
+
+ if ($loc === 'input') {
+ return (strlen($this->input_css) / 1000);
+ } else {
+ return (strlen($this->output_css_plain) / 1000);
+ }
+ }
+
+}
diff --git a/plugins/jetpack/modules/custom-css/csstidy/cssparse-rtl.css b/plugins/jetpack/modules/custom-css/csstidy/cssparse-rtl.css
new file mode 100644
index 00000000..54a7a9c9
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/cssparse-rtl.css
@@ -0,0 +1,119 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+@import url("modules/custom-css/csstidy/cssparsed.css");
+
+html, body {
+font:0.8em Verdana,Helvetica,sans-serif;
+background:#F8F8F6;
+}
+
+code {
+font-size:1.2em;
+}
+
+div#rightcol {
+padding-right:32em;
+}
+
+fieldset {
+display:block;
+margin:0.5em 0;
+padding:1em;
+border:solid #7284AB 2px;
+}
+fieldset.code_output {
+display:inline;
+}
+
+h1 {
+font-size:2em;
+}
+
+small {
+font-size:0.7em;
+}
+
+fieldset#field_input {
+float:right;
+margin:0 0 1em 0.5em;
+}
+
+fieldset#options,fieldset#code_layout {
+width:31em;
+}
+
+input#submit {
+clear:both;
+display:block;
+margin:1em;
+}
+
+select {
+margin:2px 0 0;
+}
+
+label.block {
+display:block;
+}
+
+legend {
+background:#c4E1C3;
+padding:2px 4px;
+border:dashed 1px;
+}
+
+textarea#css_text {
+width:27em;
+height:370px;
+display:block;
+margin-left:1em;
+}
+
+.help {
+cursor:help;
+}
+
+p.important {
+border:solid 1px red;
+font-weight:bold;
+padding:1em;
+background:white;
+}
+
+p {
+margin:1em 0;
+}
+
+dl {
+padding-right:0.5em;
+}
+
+dt {
+font-weight:bold;
+margin:0;
+float:right;
+clear:both;
+height:1.5em;
+}
+
+dd {
+margin:0 4em 0 0;
+height:1.5em;
+}
+
+fieldset#messages {
+background:white;
+padding:0 1em 0 0;
+}
+
+fieldset#messages div {
+height:10em;
+overflow:auto;
+}
+
+dd.Warning {
+color:orange;
+}
+
+dd.Information {
+color:green;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/csstidy/cssparse-rtl.min.css b/plugins/jetpack/modules/custom-css/csstidy/cssparse-rtl.min.css
new file mode 100644
index 00000000..02da7f4c
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/cssparse-rtl.min.css
@@ -0,0 +1 @@
+code#copytext{white-space:pre;font-family:Verdana}.at{color:#00008b}.format{color:gray}.property{color:green}.selector{color:#00f}.value{color:red;left:500px}.comment{color:orange}body,html{font:.8em Verdana,Helvetica,sans-serif;background:#f8f8f6}code{font-size:1.2em}div#rightcol{padding-right:32em}fieldset{display:block;margin:.5em 0;padding:1em;border:solid #7284ab 2px}fieldset.code_output{display:inline}h1{font-size:2em}small{font-size:.7em}fieldset#field_input{float:right;margin:0 0 1em .5em}fieldset#code_layout,fieldset#options{width:31em}input#submit{clear:both;display:block;margin:1em}select{margin:2px 0 0}label.block{display:block}legend{background:#c4e1c3;padding:2px 4px;border:dashed 1px}textarea#css_text{width:27em;height:370px;display:block;margin-left:1em}.help{cursor:help}p.important{border:solid 1px red;font-weight:700;padding:1em;background:#fff}p{margin:1em 0}dl{padding-right:.5em}dt{font-weight:700;margin:0;float:right;clear:both;height:1.5em}dd{margin:0 4em 0 0;height:1.5em}fieldset#messages{background:#fff;padding:0 1em 0 0}fieldset#messages div{height:10em;overflow:auto}dd.Warning{color:orange}dd.Information{color:green} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/csstidy/cssparse.css b/plugins/jetpack/modules/custom-css/csstidy/cssparse.css
new file mode 100644
index 00000000..bddd34f9
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/cssparse.css
@@ -0,0 +1,118 @@
+@import url("modules/custom-css/csstidy/cssparsed.css");
+
+html, body {
+font:0.8em Verdana,Helvetica,sans-serif;
+background:#F8F8F6;
+}
+
+code {
+font-size:1.2em;
+}
+
+div#rightcol {
+padding-left:32em;
+}
+
+fieldset {
+display:block;
+margin:0.5em 0;
+padding:1em;
+border:solid #7284AB 2px;
+}
+fieldset.code_output {
+display:inline;
+}
+
+h1 {
+font-size:2em;
+}
+
+small {
+font-size:0.7em;
+}
+
+fieldset#field_input {
+float:left;
+margin:0 0.5em 1em 0;
+}
+
+fieldset#options,fieldset#code_layout {
+width:31em;
+}
+
+input#submit {
+clear:both;
+display:block;
+margin:1em;
+}
+
+select {
+margin:2px 0 0;
+}
+
+label.block {
+display:block;
+}
+
+legend {
+background:#c4E1C3;
+padding:2px 4px;
+border:dashed 1px;
+}
+
+textarea#css_text {
+width:27em;
+height:370px;
+display:block;
+margin-right:1em;
+}
+
+.help {
+cursor:help;
+}
+
+p.important {
+border:solid 1px red;
+font-weight:bold;
+padding:1em;
+background:white;
+}
+
+p {
+margin:1em 0;
+}
+
+dl {
+padding-left:0.5em;
+}
+
+dt {
+font-weight:bold;
+margin:0;
+float:left;
+clear:both;
+height:1.5em;
+}
+
+dd {
+margin:0 0 0 4em;
+height:1.5em;
+}
+
+fieldset#messages {
+background:white;
+padding:0 0 0 1em;
+}
+
+fieldset#messages div {
+height:10em;
+overflow:auto;
+}
+
+dd.Warning {
+color:orange;
+}
+
+dd.Information {
+color:green;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/csstidy/cssparse.min.css b/plugins/jetpack/modules/custom-css/csstidy/cssparse.min.css
new file mode 100644
index 00000000..fa4927e5
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/cssparse.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+code#copytext{white-space:pre;font-family:Verdana}.at{color:#00008b}.format{color:gray}.property{color:green}.selector{color:#00f}.value{color:red;left:500px}.comment{color:orange}body,html{font:.8em Verdana,Helvetica,sans-serif;background:#f8f8f6}code{font-size:1.2em}div#rightcol{padding-left:32em}fieldset{display:block;margin:.5em 0;padding:1em;border:solid #7284ab 2px}fieldset.code_output{display:inline}h1{font-size:2em}small{font-size:.7em}fieldset#field_input{float:left;margin:0 .5em 1em 0}fieldset#code_layout,fieldset#options{width:31em}input#submit{clear:both;display:block;margin:1em}select{margin:2px 0 0}label.block{display:block}legend{background:#c4e1c3;padding:2px 4px;border:dashed 1px}textarea#css_text{width:27em;height:370px;display:block;margin-right:1em}.help{cursor:help}p.important{border:solid 1px red;font-weight:700;padding:1em;background:#fff}p{margin:1em 0}dl{padding-left:.5em}dt{font-weight:700;margin:0;float:left;clear:both;height:1.5em}dd{margin:0 0 0 4em;height:1.5em}fieldset#messages{background:#fff;padding:0 0 0 1em}fieldset#messages div{height:10em;overflow:auto}dd.Warning{color:orange}dd.Information{color:green} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/csstidy/cssparsed-rtl.css b/plugins/jetpack/modules/custom-css/csstidy/cssparsed-rtl.css
new file mode 100644
index 00000000..d765e2d7
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/cssparsed-rtl.css
@@ -0,0 +1,30 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+code#copytext {
+ white-space: pre;
+ font-family: Verdana;
+}
+
+.at {
+color:darkblue;
+}
+
+.format {
+color:gray;
+}
+
+.property {
+color:green;
+}
+
+.selector {
+color:blue;
+}
+
+.value {
+color:red;
+right: 500px;
+}
+
+.comment {
+color:orange;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/csstidy/cssparsed-rtl.min.css b/plugins/jetpack/modules/custom-css/csstidy/cssparsed-rtl.min.css
new file mode 100644
index 00000000..ba9903d0
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/cssparsed-rtl.min.css
@@ -0,0 +1 @@
+code#copytext{white-space:pre;font-family:Verdana}.at{color:#00008b}.format{color:gray}.property{color:green}.selector{color:#00f}.value{color:red;right:500px}.comment{color:orange} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/csstidy/cssparsed.css b/plugins/jetpack/modules/custom-css/csstidy/cssparsed.css
new file mode 100644
index 00000000..5aaf2bb1
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/cssparsed.css
@@ -0,0 +1,29 @@
+code#copytext {
+ white-space: pre;
+ font-family: Verdana;
+}
+
+.at {
+color:darkblue;
+}
+
+.format {
+color:gray;
+}
+
+.property {
+color:green;
+}
+
+.selector {
+color:blue;
+}
+
+.value {
+color:red;
+left: 500px;
+}
+
+.comment {
+color:orange;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/csstidy/cssparsed.min.css b/plugins/jetpack/modules/custom-css/csstidy/cssparsed.min.css
new file mode 100644
index 00000000..db96935e
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/cssparsed.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+code#copytext{white-space:pre;font-family:Verdana}.at{color:#00008b}.format{color:gray}.property{color:green}.selector{color:#00f}.value{color:red;left:500px}.comment{color:orange} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/csstidy/data-wp.inc.php b/plugins/jetpack/modules/custom-css/csstidy/data-wp.inc.php
new file mode 100644
index 00000000..f0f7376f
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/data-wp.inc.php
@@ -0,0 +1,102 @@
+<?php
+
+unset( $GLOBALS['csstidy']['all_properties']['binding'] );
+
+$GLOBALS['csstidy']['all_properties']['text-size-adjust'] = 'CSS3.0';
+
+// Support browser prefixes for properties only in the latest CSS draft
+foreach ( $GLOBALS['csstidy']['all_properties'] as $property => $levels ) {
+ if ( strpos( $levels, "," ) === false ) {
+ $GLOBALS['csstidy']['all_properties']['-moz-' . $property] = $levels;
+ $GLOBALS['csstidy']['all_properties']['-webkit-' . $property] = $levels;
+ $GLOBALS['csstidy']['all_properties']['-ms-' . $property] = $levels;
+ $GLOBALS['csstidy']['all_properties']['-o-' . $property] = $levels;
+ $GLOBALS['csstidy']['all_properties']['-khtml-' . $property] = $levels;
+
+ if ( in_array( $property, $GLOBALS['csstidy']['unit_values'] ) ) {
+ $GLOBALS['csstidy']['unit_values'][] = '-moz-' . $property;
+ $GLOBALS['csstidy']['unit_values'][] = '-webkit-' . $property;
+ $GLOBALS['csstidy']['unit_values'][] = '-ms-' . $property;
+ $GLOBALS['csstidy']['unit_values'][] = '-o-' . $property;
+ $GLOBALS['csstidy']['unit_values'][] = '-khtml-' . $property;
+ }
+
+ if ( in_array( $property, $GLOBALS['csstidy']['color_values'] ) ) {
+ $GLOBALS['csstidy']['color_values'][] = '-moz-' . $property;
+ $GLOBALS['csstidy']['color_values'][] = '-webkit-' . $property;
+ $GLOBALS['csstidy']['color_values'][] = '-ms-' . $property;
+ $GLOBALS['csstidy']['color_values'][] = '-o-' . $property;
+ $GLOBALS['csstidy']['color_values'][] = '-khtml-' . $property;
+ }
+ }
+}
+
+// Add `display` to the list of properties that can be used multiple times in a single selector
+$GLOBALS['csstidy']['multiple_properties'][] = 'display';
+
+// Allow vendor prefixes for any property that is allowed to be used multiple times inside a single selector
+foreach ( $GLOBALS['csstidy']['multiple_properties'] as $property ) {
+ if ( '-' != $property[0] ) {
+ $GLOBALS['csstidy']['multiple_properties'][] = '-o-' . $property;
+ $GLOBALS['csstidy']['multiple_properties'][] = '-ms-' . $property;
+ $GLOBALS['csstidy']['multiple_properties'][] = '-webkit-' . $property;
+ $GLOBALS['csstidy']['multiple_properties'][] = '-moz-' . $property;
+ $GLOBALS['csstidy']['multiple_properties'][] = '-khtml-' . $property;
+ }
+}
+
+/**
+ * CSS Animation
+ *
+ * @see https://developer.mozilla.org/en/CSS/CSS_animations
+ */
+$GLOBALS['csstidy']['at_rules']['-webkit-keyframes'] = 'at';
+$GLOBALS['csstidy']['at_rules']['-moz-keyframes'] = 'at';
+$GLOBALS['csstidy']['at_rules']['-ms-keyframes'] = 'at';
+$GLOBALS['csstidy']['at_rules']['-o-keyframes'] = 'at';
+
+/**
+ * Non-standard viewport rule.
+ */
+$GLOBALS['csstidy']['at_rules']['viewport'] = 'is';
+$GLOBALS['csstidy']['at_rules']['-webkit-viewport'] = 'is';
+$GLOBALS['csstidy']['at_rules']['-moz-viewport'] = 'is';
+$GLOBALS['csstidy']['at_rules']['-ms-viewport'] = 'is';
+
+/**
+ * Non-standard CSS properties. They're not part of any spec, but we say
+ * they're in all of them so that we can support them.
+ */
+$GLOBALS['csstidy']['all_properties']['-webkit-filter'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-moz-filter'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-ms-filter'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['filter'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['scrollbar-face-color'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-ms-interpolation-mode'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-rendering'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-webkit-transform-origin-x'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-webkit-transform-origin-y'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-webkit-transform-origin-z'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-webkit-font-smoothing'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-moz-osx-font-smoothing'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-font-smooth'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-o-object-fit'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['object-fit'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['-o-object-position'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['object-position'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-overflow'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['zoom'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['pointer-events'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-feature-settings'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-kerning'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-language-override'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-synthesis'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-variant-alternates'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-variant-caps'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-variant-east-asian'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-variant-ligatures'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-variant-numeric'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-variant-position'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-variation-settings'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['line-height-step'] = 'CSS3.0';
+
diff --git a/plugins/jetpack/modules/custom-css/csstidy/data.inc.php b/plugins/jetpack/modules/custom-css/csstidy/data.inc.php
new file mode 100644
index 00000000..9ed2e1c4
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/data.inc.php
@@ -0,0 +1,693 @@
+<?php
+/**
+ * Various CSS Data for CSSTidy
+ *
+ * This file is part of CSSTidy.
+ *
+ * CSSTidy is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * CSSTidy is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with CSSTidy; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * @license http://opensource.org/licenses/gpl-license.php GNU Public License
+ * @package csstidy
+ * @author Florian Schmitz (floele at gmail dot com) 2005
+ * @author Nikolay Matsievsky (speed at webo dot name) 2010
+ */
+
+define('AT_START', 1);
+define('AT_END', 2);
+define('SEL_START', 3);
+define('SEL_END', 4);
+define('PROPERTY', 5);
+define('VALUE', 6);
+define('COMMENT', 7);
+define('DEFAULT_AT', 41);
+
+/**
+ * All whitespace allowed in CSS
+ *
+ * @global array $GLOBALS['csstidy']['whitespace']
+ * @version 1.0
+ */
+$GLOBALS['csstidy']['whitespace'] = array(' ',"\n","\t","\r","\x0B");
+
+/**
+ * All CSS tokens used by csstidy
+ *
+ * @global string $GLOBALS['csstidy']['tokens']
+ * @version 1.0
+ */
+$GLOBALS['csstidy']['tokens'] = '/@}{;:=\'"(,\\!$%&)*+.<>?[]^`|~';
+
+/**
+ * All CSS units (CSS 3 units included)
+ *
+ * @see compress_numbers()
+ * @global array $GLOBALS['csstidy']['units']
+ * @version 1.0
+ */
+$GLOBALS['csstidy']['units'] = array('in','cm','mm','pt','pc','px','rem','em','%','ex','gd','vw','vh','vm','deg','grad','rad','ms','s','khz','hz');
+
+/**
+ * Available at-rules
+ *
+ * @global array $GLOBALS['csstidy']['at_rules']
+ * @version 1.0
+ */
+$GLOBALS['csstidy']['at_rules'] = array('page' => 'is','font-face' => 'is','charset' => 'iv', 'import' => 'iv','namespace' => 'iv','media' => 'at','keyframes' => 'at', 'supports' => 'at');
+
+ /**
+ * Properties that need a value with unit
+ *
+ * @todo CSS3 properties
+ * @see compress_numbers();
+ * @global array $GLOBALS['csstidy']['unit_values']
+ * @version 1.2
+ */
+$GLOBALS['csstidy']['unit_values'] = array ('background', 'background-position', 'background-size', 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-width',
+ 'border-top-width', 'border-right-width', 'border-left-width', 'border-bottom-width', 'bottom', 'border-spacing', 'column-gap', 'column-width',
+ 'font-size', 'height', 'left', 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', 'max-height',
+ 'max-width', 'min-height', 'min-width', 'outline', 'outline-width', 'padding', 'padding-top', 'padding-right',
+ 'padding-bottom', 'padding-left', 'perspective', 'right', 'top', 'text-indent', 'letter-spacing', 'word-spacing', 'width');
+
+/**
+ * Properties that allow <color> as value
+ *
+ * @todo CSS3 properties
+ * @see compress_numbers();
+ * @global array $GLOBALS['csstidy']['color_values']
+ * @version 1.0
+ */
+$GLOBALS['csstidy']['color_values'] = array();
+$GLOBALS['csstidy']['color_values'][] = 'background-color';
+$GLOBALS['csstidy']['color_values'][] = 'border-color';
+$GLOBALS['csstidy']['color_values'][] = 'border-top-color';
+$GLOBALS['csstidy']['color_values'][] = 'border-right-color';
+$GLOBALS['csstidy']['color_values'][] = 'border-bottom-color';
+$GLOBALS['csstidy']['color_values'][] = 'border-left-color';
+$GLOBALS['csstidy']['color_values'][] = 'color';
+$GLOBALS['csstidy']['color_values'][] = 'outline-color';
+$GLOBALS['csstidy']['color_values'][] = 'column-rule-color';
+
+/**
+ * Default values for the background properties
+ *
+ * @todo Possibly property names will change during CSS3 development
+ * @global array $GLOBALS['csstidy']['background_prop_default']
+ * @see dissolve_short_bg()
+ * @see merge_bg()
+ * @version 1.0
+ */
+$GLOBALS['csstidy']['background_prop_default'] = array();
+$GLOBALS['csstidy']['background_prop_default']['background-image'] = 'none';
+$GLOBALS['csstidy']['background_prop_default']['background-size'] = 'auto';
+$GLOBALS['csstidy']['background_prop_default']['background-repeat'] = 'repeat';
+$GLOBALS['csstidy']['background_prop_default']['background-position'] = '0 0';
+$GLOBALS['csstidy']['background_prop_default']['background-attachment'] = 'scroll';
+$GLOBALS['csstidy']['background_prop_default']['background-clip'] = 'border';
+$GLOBALS['csstidy']['background_prop_default']['background-origin'] = 'padding';
+$GLOBALS['csstidy']['background_prop_default']['background-color'] = 'transparent';
+
+/**
+ * Default values for the font properties
+ *
+ * @global array $GLOBALS['csstidy']['font_prop_default']
+ * @see merge_fonts()
+ * @version 1.3
+ */
+$GLOBALS['csstidy']['font_prop_default'] = array();
+$GLOBALS['csstidy']['font_prop_default']['font-style'] = 'normal';
+$GLOBALS['csstidy']['font_prop_default']['font-variant'] = 'normal';
+$GLOBALS['csstidy']['font_prop_default']['font-weight'] = 'normal';
+$GLOBALS['csstidy']['font_prop_default']['font-size'] = '';
+$GLOBALS['csstidy']['font_prop_default']['line-height'] = '';
+$GLOBALS['csstidy']['font_prop_default']['font-family'] = '';
+
+/**
+ * A list of non-W3C color names which get replaced by their hex-codes
+ *
+ * @global array $GLOBALS['csstidy']['replace_colors']
+ * @see cut_color()
+ * @version 1.0
+ */
+$GLOBALS['csstidy']['replace_colors'] = array();
+$GLOBALS['csstidy']['replace_colors']['aliceblue'] = '#f0f8ff';
+$GLOBALS['csstidy']['replace_colors']['antiquewhite'] = '#faebd7';
+$GLOBALS['csstidy']['replace_colors']['aquamarine'] = '#7fffd4';
+$GLOBALS['csstidy']['replace_colors']['azure'] = '#f0ffff';
+$GLOBALS['csstidy']['replace_colors']['beige'] = '#f5f5dc';
+$GLOBALS['csstidy']['replace_colors']['bisque'] = '#ffe4c4';
+$GLOBALS['csstidy']['replace_colors']['blanchedalmond'] = '#ffebcd';
+$GLOBALS['csstidy']['replace_colors']['blueviolet'] = '#8a2be2';
+$GLOBALS['csstidy']['replace_colors']['brown'] = '#a52a2a';
+$GLOBALS['csstidy']['replace_colors']['burlywood'] = '#deb887';
+$GLOBALS['csstidy']['replace_colors']['cadetblue'] = '#5f9ea0';
+$GLOBALS['csstidy']['replace_colors']['chartreuse'] = '#7fff00';
+$GLOBALS['csstidy']['replace_colors']['chocolate'] = '#d2691e';
+$GLOBALS['csstidy']['replace_colors']['coral'] = '#ff7f50';
+$GLOBALS['csstidy']['replace_colors']['cornflowerblue'] = '#6495ed';
+$GLOBALS['csstidy']['replace_colors']['cornsilk'] = '#fff8dc';
+$GLOBALS['csstidy']['replace_colors']['crimson'] = '#dc143c';
+$GLOBALS['csstidy']['replace_colors']['cyan'] = '#00ffff';
+$GLOBALS['csstidy']['replace_colors']['darkblue'] = '#00008b';
+$GLOBALS['csstidy']['replace_colors']['darkcyan'] = '#008b8b';
+$GLOBALS['csstidy']['replace_colors']['darkgoldenrod'] = '#b8860b';
+$GLOBALS['csstidy']['replace_colors']['darkgray'] = '#a9a9a9';
+$GLOBALS['csstidy']['replace_colors']['darkgreen'] = '#006400';
+$GLOBALS['csstidy']['replace_colors']['darkkhaki'] = '#bdb76b';
+$GLOBALS['csstidy']['replace_colors']['darkmagenta'] = '#8b008b';
+$GLOBALS['csstidy']['replace_colors']['darkolivegreen'] = '#556b2f';
+$GLOBALS['csstidy']['replace_colors']['darkorange'] = '#ff8c00';
+$GLOBALS['csstidy']['replace_colors']['darkorchid'] = '#9932cc';
+$GLOBALS['csstidy']['replace_colors']['darkred'] = '#8b0000';
+$GLOBALS['csstidy']['replace_colors']['darksalmon'] = '#e9967a';
+$GLOBALS['csstidy']['replace_colors']['darkseagreen'] = '#8fbc8f';
+$GLOBALS['csstidy']['replace_colors']['darkslateblue'] = '#483d8b';
+$GLOBALS['csstidy']['replace_colors']['darkslategray'] = '#2f4f4f';
+$GLOBALS['csstidy']['replace_colors']['darkturquoise'] = '#00ced1';
+$GLOBALS['csstidy']['replace_colors']['darkviolet'] = '#9400d3';
+$GLOBALS['csstidy']['replace_colors']['deeppink'] = '#ff1493';
+$GLOBALS['csstidy']['replace_colors']['deepskyblue'] = '#00bfff';
+$GLOBALS['csstidy']['replace_colors']['dimgray'] = '#696969';
+$GLOBALS['csstidy']['replace_colors']['dodgerblue'] = '#1e90ff';
+$GLOBALS['csstidy']['replace_colors']['feldspar'] = '#d19275';
+$GLOBALS['csstidy']['replace_colors']['firebrick'] = '#b22222';
+$GLOBALS['csstidy']['replace_colors']['floralwhite'] = '#fffaf0';
+$GLOBALS['csstidy']['replace_colors']['forestgreen'] = '#228b22';
+$GLOBALS['csstidy']['replace_colors']['gainsboro'] = '#dcdcdc';
+$GLOBALS['csstidy']['replace_colors']['ghostwhite'] = '#f8f8ff';
+$GLOBALS['csstidy']['replace_colors']['gold'] = '#ffd700';
+$GLOBALS['csstidy']['replace_colors']['goldenrod'] = '#daa520';
+$GLOBALS['csstidy']['replace_colors']['greenyellow'] = '#adff2f';
+$GLOBALS['csstidy']['replace_colors']['honeydew'] = '#f0fff0';
+$GLOBALS['csstidy']['replace_colors']['hotpink'] = '#ff69b4';
+$GLOBALS['csstidy']['replace_colors']['indianred'] = '#cd5c5c';
+$GLOBALS['csstidy']['replace_colors']['indigo'] = '#4b0082';
+$GLOBALS['csstidy']['replace_colors']['ivory'] = '#fffff0';
+$GLOBALS['csstidy']['replace_colors']['khaki'] = '#f0e68c';
+$GLOBALS['csstidy']['replace_colors']['lavender'] = '#e6e6fa';
+$GLOBALS['csstidy']['replace_colors']['lavenderblush'] = '#fff0f5';
+$GLOBALS['csstidy']['replace_colors']['lawngreen'] = '#7cfc00';
+$GLOBALS['csstidy']['replace_colors']['lemonchiffon'] = '#fffacd';
+$GLOBALS['csstidy']['replace_colors']['lightblue'] = '#add8e6';
+$GLOBALS['csstidy']['replace_colors']['lightcoral'] = '#f08080';
+$GLOBALS['csstidy']['replace_colors']['lightcyan'] = '#e0ffff';
+$GLOBALS['csstidy']['replace_colors']['lightgoldenrodyellow'] = '#fafad2';
+$GLOBALS['csstidy']['replace_colors']['lightgrey'] = '#d3d3d3';
+$GLOBALS['csstidy']['replace_colors']['lightgreen'] = '#90ee90';
+$GLOBALS['csstidy']['replace_colors']['lightpink'] = '#ffb6c1';
+$GLOBALS['csstidy']['replace_colors']['lightsalmon'] = '#ffa07a';
+$GLOBALS['csstidy']['replace_colors']['lightseagreen'] = '#20b2aa';
+$GLOBALS['csstidy']['replace_colors']['lightskyblue'] = '#87cefa';
+$GLOBALS['csstidy']['replace_colors']['lightslateblue'] = '#8470ff';
+$GLOBALS['csstidy']['replace_colors']['lightslategray'] = '#778899';
+$GLOBALS['csstidy']['replace_colors']['lightsteelblue'] = '#b0c4de';
+$GLOBALS['csstidy']['replace_colors']['lightyellow'] = '#ffffe0';
+$GLOBALS['csstidy']['replace_colors']['limegreen'] = '#32cd32';
+$GLOBALS['csstidy']['replace_colors']['linen'] = '#faf0e6';
+$GLOBALS['csstidy']['replace_colors']['magenta'] = '#ff00ff';
+$GLOBALS['csstidy']['replace_colors']['mediumaquamarine'] = '#66cdaa';
+$GLOBALS['csstidy']['replace_colors']['mediumblue'] = '#0000cd';
+$GLOBALS['csstidy']['replace_colors']['mediumorchid'] = '#ba55d3';
+$GLOBALS['csstidy']['replace_colors']['mediumpurple'] = '#9370d8';
+$GLOBALS['csstidy']['replace_colors']['mediumseagreen'] = '#3cb371';
+$GLOBALS['csstidy']['replace_colors']['mediumslateblue'] = '#7b68ee';
+$GLOBALS['csstidy']['replace_colors']['mediumspringgreen'] = '#00fa9a';
+$GLOBALS['csstidy']['replace_colors']['mediumturquoise'] = '#48d1cc';
+$GLOBALS['csstidy']['replace_colors']['mediumvioletred'] = '#c71585';
+$GLOBALS['csstidy']['replace_colors']['midnightblue'] = '#191970';
+$GLOBALS['csstidy']['replace_colors']['mintcream'] = '#f5fffa';
+$GLOBALS['csstidy']['replace_colors']['mistyrose'] = '#ffe4e1';
+$GLOBALS['csstidy']['replace_colors']['moccasin'] = '#ffe4b5';
+$GLOBALS['csstidy']['replace_colors']['navajowhite'] = '#ffdead';
+$GLOBALS['csstidy']['replace_colors']['oldlace'] = '#fdf5e6';
+$GLOBALS['csstidy']['replace_colors']['olivedrab'] = '#6b8e23';
+$GLOBALS['csstidy']['replace_colors']['orangered'] = '#ff4500';
+$GLOBALS['csstidy']['replace_colors']['orchid'] = '#da70d6';
+$GLOBALS['csstidy']['replace_colors']['palegoldenrod'] = '#eee8aa';
+$GLOBALS['csstidy']['replace_colors']['palegreen'] = '#98fb98';
+$GLOBALS['csstidy']['replace_colors']['paleturquoise'] = '#afeeee';
+$GLOBALS['csstidy']['replace_colors']['palevioletred'] = '#d87093';
+$GLOBALS['csstidy']['replace_colors']['papayawhip'] = '#ffefd5';
+$GLOBALS['csstidy']['replace_colors']['peachpuff'] = '#ffdab9';
+$GLOBALS['csstidy']['replace_colors']['peru'] = '#cd853f';
+$GLOBALS['csstidy']['replace_colors']['pink'] = '#ffc0cb';
+$GLOBALS['csstidy']['replace_colors']['plum'] = '#dda0dd';
+$GLOBALS['csstidy']['replace_colors']['powderblue'] = '#b0e0e6';
+$GLOBALS['csstidy']['replace_colors']['rosybrown'] = '#bc8f8f';
+$GLOBALS['csstidy']['replace_colors']['royalblue'] = '#4169e1';
+$GLOBALS['csstidy']['replace_colors']['saddlebrown'] = '#8b4513';
+$GLOBALS['csstidy']['replace_colors']['salmon'] = '#fa8072';
+$GLOBALS['csstidy']['replace_colors']['sandybrown'] = '#f4a460';
+$GLOBALS['csstidy']['replace_colors']['seagreen'] = '#2e8b57';
+$GLOBALS['csstidy']['replace_colors']['seashell'] = '#fff5ee';
+$GLOBALS['csstidy']['replace_colors']['sienna'] = '#a0522d';
+$GLOBALS['csstidy']['replace_colors']['skyblue'] = '#87ceeb';
+$GLOBALS['csstidy']['replace_colors']['slateblue'] = '#6a5acd';
+$GLOBALS['csstidy']['replace_colors']['slategray'] = '#708090';
+$GLOBALS['csstidy']['replace_colors']['snow'] = '#fffafa';
+$GLOBALS['csstidy']['replace_colors']['springgreen'] = '#00ff7f';
+$GLOBALS['csstidy']['replace_colors']['steelblue'] = '#4682b4';
+$GLOBALS['csstidy']['replace_colors']['tan'] = '#d2b48c';
+$GLOBALS['csstidy']['replace_colors']['thistle'] = '#d8bfd8';
+$GLOBALS['csstidy']['replace_colors']['tomato'] = '#ff6347';
+$GLOBALS['csstidy']['replace_colors']['turquoise'] = '#40e0d0';
+$GLOBALS['csstidy']['replace_colors']['violet'] = '#ee82ee';
+$GLOBALS['csstidy']['replace_colors']['violetred'] = '#d02090';
+$GLOBALS['csstidy']['replace_colors']['wheat'] = '#f5deb3';
+$GLOBALS['csstidy']['replace_colors']['whitesmoke'] = '#f5f5f5';
+$GLOBALS['csstidy']['replace_colors']['yellowgreen'] = '#9acd32';
+
+/**
+ * A list of all shorthand properties that are divided into four properties and/or have four subvalues
+ *
+ * @global array $GLOBALS['csstidy']['shorthands']
+ * @todo Are there new ones in CSS3?
+ * @see dissolve_4value_shorthands()
+ * @see merge_4value_shorthands()
+ * @version 1.0
+ */
+$GLOBALS['csstidy']['shorthands'] = array();
+$GLOBALS['csstidy']['shorthands']['border-color'] = array('border-top-color','border-right-color','border-bottom-color','border-left-color');
+$GLOBALS['csstidy']['shorthands']['border-style'] = array('border-top-style','border-right-style','border-bottom-style','border-left-style');
+$GLOBALS['csstidy']['shorthands']['border-width'] = array('border-top-width','border-right-width','border-bottom-width','border-left-width');
+$GLOBALS['csstidy']['shorthands']['margin'] = array('margin-top','margin-right','margin-bottom','margin-left');
+$GLOBALS['csstidy']['shorthands']['padding'] = array('padding-top','padding-right','padding-bottom','padding-left');
+$GLOBALS['csstidy']['shorthands']['-moz-border-radius'] = 0;
+
+/**
+ * All CSS Properties. Needed for csstidy::property_is_next()
+ *
+ * @global array $GLOBALS['csstidy']['all_properties']
+ * @todo Add CSS3 properties
+ * @version 1.0
+ * @see csstidy::property_is_next()
+ */
+$GLOBALS['csstidy']['all_properties']['align-content'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['align-items'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['align-self'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['alignment-adjust'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['alignment-baseline'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['animation'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['animation-delay'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['animation-direction'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['animation-duration'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['animation-fill-mode'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['animation-iteration-count'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['animation-name'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['animation-play-state'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['animation-timing-function'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['appearance'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['azimuth'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['backface-visibility'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['background'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['background-attachment'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['background-clip'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['background-color'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['background-image'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['background-origin'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['background-position'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['background-repeat'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['background-size'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['baseline-shift'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['binding'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['bleed'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['bookmark-label'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['bookmark-level'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['bookmark-state'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['bookmark-target'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-bottom'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-bottom-color'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-bottom-left-radius'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-bottom-right-radius'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-bottom-style'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-bottom-width'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-collapse'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-color'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-image'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-image-outset'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-image-repeat'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-image-slice'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-image-source'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-image-width'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-left'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-left-color'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-left-style'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-left-width'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-radius'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-right'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-right-color'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-right-style'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-right-width'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-spacing'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-style'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-top'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-top-color'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-top-left-radius'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-top-right-radius'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-top-style'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-top-width'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['border-width'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['bottom'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['box-decoration-break'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['box-shadow'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['box-sizing'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['break-after'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['break-before'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['break-inside'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['caption-side'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['clear'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['clip'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['color'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['color-profile'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['column-count'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['column-fill'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['column-gap'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['column-rule'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['column-rule-color'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['column-rule-style'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['column-rule-width'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['column-span'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['column-width'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['columns'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['content'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['counter-increment'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['counter-reset'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['crop'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['cue'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['cue-after'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['cue-before'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['cursor'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['direction'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['display'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['dominant-baseline'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['drop-initial-after-adjust'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['drop-initial-after-align'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['drop-initial-before-adjust'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['drop-initial-before-align'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['drop-initial-size'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['drop-initial-value'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['elevation'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['empty-cells'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['fill'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['fit'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['fit-position'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex-align'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex-basis'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex-direction'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex-flow'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex-grow'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex-line-pack'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex-order'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex-pack'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex-shrink'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['flex-wrap'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['float'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['float-offset'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-family'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-size'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-size-adjust'] = 'CSS2.0,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-stretch'] = 'CSS2.0,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-style'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-variant'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['font-weight'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-area'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-auto-columns'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-auto-flow'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-auto-rows'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-column'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-columns'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-column-end'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-column-gap'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-column-start'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-gap'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-row'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-rows'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-row-end'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-row-gap'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-row-start'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-template'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-template-areas'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-template-columns'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['grid-template-rows'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['hanging-punctuation'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['height'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['hyphenate-after'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['hyphenate-before'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['hyphenate-character'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['hyphenate-lines'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['hyphenate-resource'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['hyphens'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['icon'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['image-orientation'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['image-rendering'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['image-resolution'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['inline-box-align'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['justify-content'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['justify-items'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['justify-self'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['left'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['letter-spacing'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['line-break'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['line-height'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['line-stacking'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['line-stacking-ruby'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['line-stacking-shift'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['line-stacking-strategy'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['list-style'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['list-style-image'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['list-style-position'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['list-style-type'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['margin'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['margin-bottom'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['margin-left'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['margin-right'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['margin-top'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['marker-offset'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['marks'] = 'CSS2.0,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['marquee-direction'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['marquee-loop'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['marquee-play-count'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['marquee-speed'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['marquee-style'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['max-height'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['max-width'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['min-height'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['min-width'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['move-to'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['nav-down'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['nav-index'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['nav-left'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['nav-right'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['nav-up'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['opacity'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['order'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['orphans'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['outline'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['outline-color'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['outline-offset'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['outline-style'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['outline-width'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['overflow'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['overflow-style'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['overflow-wrap'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['overflow-x'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['overflow-y'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['padding'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['padding-bottom'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['padding-left'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['padding-right'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['padding-top'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['page'] = 'CSS2.0,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['page-break-after'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['page-break-before'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['page-break-inside'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['page-policy'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['pause'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['pause-after'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['pause-before'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['perspective'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['perspective-origin'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['phonemes'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['pitch'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['pitch-range'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['play-during'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['position'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['presentation-level'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['punctuation-trim'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['quotes'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['rendering-intent'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['resize'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['rest'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['rest-after'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['rest-before'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['richness'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['right'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['rotation'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['rotation-point'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['ruby-align'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['ruby-overhang'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['ruby-position'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['ruby-span'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['size'] = 'CSS2.0,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['speak'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['speak-header'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['speak-numeral'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['speak-punctuation'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['speech-rate'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['src'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['stress'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['string-set'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['stroke'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['tab-size'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['table-layout'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['target'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['target-name'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['target-new'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['target-position'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-align'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-align-last'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-decoration'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-decoration-color'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-decoration-line'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-decoration-skip'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-decoration-style'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-emphasis'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-emphasis-color'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-emphasis-position'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-emphasis-style'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-height'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-indent'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-justify'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-outline'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-shadow'] = 'CSS2.0,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-space-collapse'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-transform'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-underline-position'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['text-wrap'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['top'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['transform'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['transform-origin'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['transform-style'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['transition'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['transition-delay'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['transition-duration'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['transition-property'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['transition-timing-function'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['unicode-bidi'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['vertical-align'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['visibility'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['voice-balance'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['voice-duration'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['voice-family'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['voice-pitch'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['voice-pitch-range'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['voice-rate'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['voice-stress'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['voice-volume'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['volume'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['white-space'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['widows'] = 'CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['width'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['word-break'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['word-spacing'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0';
+$GLOBALS['csstidy']['all_properties']['word-wrap'] = 'CSS3.0';
+$GLOBALS['csstidy']['all_properties']['z-index'] = 'CSS2.0,CSS2.1,CSS3.0';
+
+/**
+ * An array containing all properties that can accept a quoted string as a value.
+ *
+ * @global array $GLOBALS['csstidy']['quoted_string_properties']
+ */
+$GLOBALS['csstidy']['quoted_string_properties'] = array('content', 'font', 'font-family', 'quotes');
+
+/**
+ * An array containing all properties that can be defined multiple times without being overwritten.
+ * All unit values are included so that units like rem can be supported with fallbacks to px or em.
+ *
+ * @global array $GLOBALS['csstidy']['quoted_string_properties']
+ */
+$GLOBALS['csstidy']['multiple_properties'] = array_merge( $GLOBALS['csstidy']['color_values'], $GLOBALS['csstidy']['unit_values'], array( 'transition', 'background-image', 'border-image', 'list-style-image' ) );
+
+/**
+ * An array containing all predefined templates.
+ *
+ * @global array $GLOBALS['csstidy']['predefined_templates']
+ * @version 1.0
+ * @see csstidy::load_template()
+ */
+$GLOBALS['csstidy']['predefined_templates']['default'][] = '<span class="at">'; //string before @rule
+$GLOBALS['csstidy']['predefined_templates']['default'][] = '</span> <span class="format">{</span>'."\n"; //bracket after @-rule
+$GLOBALS['csstidy']['predefined_templates']['default'][] = '<span class="selector">'; //string before selector
+$GLOBALS['csstidy']['predefined_templates']['default'][] = '</span> <span class="format">{</span>'."\n"; //bracket after selector
+$GLOBALS['csstidy']['predefined_templates']['default'][] = '<span class="property">'; //string before property
+$GLOBALS['csstidy']['predefined_templates']['default'][] = '</span><span class="value">'; //string after property+before value
+$GLOBALS['csstidy']['predefined_templates']['default'][] = '</span><span class="format">;</span>'."\n"; //string after value
+$GLOBALS['csstidy']['predefined_templates']['default'][] = '<span class="format">}</span>'; //closing bracket - selector
+$GLOBALS['csstidy']['predefined_templates']['default'][] = "\n\n"; //space between blocks {...}
+$GLOBALS['csstidy']['predefined_templates']['default'][] = "\n".'<span class="format">}</span>'. "\n\n"; //closing bracket @-rule
+$GLOBALS['csstidy']['predefined_templates']['default'][] = ''; //indent in @-rule
+$GLOBALS['csstidy']['predefined_templates']['default'][] = '<span class="comment">'; // before comment
+$GLOBALS['csstidy']['predefined_templates']['default'][] = '</span>'."\n"; // after comment
+$GLOBALS['csstidy']['predefined_templates']['default'][] = "\n"; // after last line @-rule
+
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '<span class="at">';
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '</span> <span class="format">{</span>'."\n";
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '<span class="selector">';
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '</span><span class="format">{</span>';
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '<span class="property">';
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '</span><span class="value">';
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '</span><span class="format">;</span>';
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '<span class="format">}</span>';
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = "\n";
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = "\n". '<span class="format">}'."\n".'</span>';
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '';
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '<span class="comment">'; // before comment
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '</span>'; // after comment
+$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = "\n";
+
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="at">';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '</span><span class="format">{</span>';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="selector">';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '</span><span class="format">{</span>';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="property">';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '</span><span class="value">';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '</span><span class="format">;</span>';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="format">}</span>';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="format">}</span>';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '';
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="comment">'; // before comment
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '</span>'; // after comment
+$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '';
+
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '<span class="at">';
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span> <span class="format">{</span>'."\n";
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '<span class="selector">';
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span>'."\n".'<span class="format">{</span>'."\n";
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = ' <span class="property">';
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span><span class="value">';
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span><span class="format">;</span>'."\n";
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '<span class="format">}</span>';
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = "\n\n";
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = "\n".'<span class="format">}</span>'."\n\n";
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = ' ';
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '<span class="comment">'; // before comment
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span>'."\n"; // after comment
+$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = "\n";
+
+require dirname( __FILE__ ) . '/data-wp.inc.php';
diff --git a/plugins/jetpack/modules/custom-css/csstidy/lang.inc.php b/plugins/jetpack/modules/custom-css/csstidy/lang.inc.php
new file mode 100644
index 00000000..d4c61114
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/lang.inc.php
@@ -0,0 +1,308 @@
+<?php
+
+/**
+ * Localization of CSS Optimiser Interface of CSSTidy
+ *
+ * Copyright 2005, 2006, 2007 Florian Schmitz
+ *
+ * This file is part of CSSTidy.
+ *
+ * CSSTidy is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * CSSTidy is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @license http://opensource.org/licenses/lgpl-license.php GNU Lesser General Public License
+ * @package csstidy
+ * @author Florian Schmitz (floele at gmail dot com) 2005-2007
+ * @author Brett Zamir (brettz9 at yahoo dot com) 2007
+ */
+
+if ( isset( $_GET['lang'] ) ) {
+ $l = $_GET['lang'];
+} elseif ( isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) {
+ $l = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
+ $l = strtolower( substr( $l, 0, 2 ) );
+} else {
+ $l = '';
+}
+
+$l = ( in_array( $l, array( 'de', 'fr', 'zh' ) ) ) ? $l : 'en';
+
+// note 5 in all but French, and 40 in all are orphaned
+
+$lang = array();
+$lang['en'][0] = 'CSS Formatter and Optimiser/Optimizer (based on CSSTidy ';
+$lang['en'][1] = 'CSS Formatter and Optimiser';
+$lang['en'][2] = '(based on';
+$lang['en'][3] = '(plaintext)';
+$lang['en'][4] = 'Important Note:';
+$lang['en'][6] = 'Your code should be well-formed. This is <strong>not a validator</strong> which points out errors in your CSS code. To make sure that your code is valid, use the <a href="http://jigsaw.w3.org/css-validator/">W3C Validator</a>.';
+$lang['en'][7] = 'all comments are removed';
+$lang['en'][8] = 'CSS Input:';
+$lang['en'][9] = 'CSS-Code:';
+$lang['en'][10] = 'CSS from URL:';
+$lang['en'][11] = 'Code Layout:';
+$lang['en'][12] = 'Compression&#160;(code&#160;layout):';
+$lang['en'][13] = 'Highest (no readability, smallest size)';
+$lang['en'][14] = 'High (moderate readability, smaller size)';
+$lang['en'][15] = 'Standard (balance between readability and size)';
+$lang['en'][16] = 'Low (higher readability)';
+$lang['en'][17] = 'Custom (enter below)';
+$lang['en'][18] = 'Custom <a href="http://csstidy.sourceforge.net/templates.php">template</a>';
+$lang['en'][19] = 'Options';
+$lang['en'][20] = 'Sort Selectors (caution)';
+$lang['en'][21] = 'Sort Properties';
+$lang['en'][22] = 'Regroup selectors';
+$lang['en'][23] = 'Optimise shorthands';
+$lang['en'][24] = 'Compress colors';
+$lang['en'][25] = 'Lowercase selectors';
+$lang['en'][26] = 'Case for properties:';
+$lang['en'][27] = 'Lowercase';
+$lang['en'][28] = 'No or invalid CSS input or wrong URL!';
+$lang['en'][29] = 'Uppercase';
+$lang['en'][30] = 'lowercase elementnames needed for XHTML';
+$lang['en'][31] = 'Remove unnecessary backslashes';
+$lang['en'][32] = 'convert !important-hack';
+$lang['en'][33] = 'Output as file';
+$lang['en'][34] = 'Bigger compression because of smaller newlines (copy &#38; paste doesn\'t work)';
+$lang['en'][35] = 'Process CSS';
+$lang['en'][36] = 'Compression Ratio';
+$lang['en'][37] = 'Input';
+$lang['en'][38] = 'Output';
+$lang['en'][39] = 'Language';
+$lang['en'][41] = 'Attention: This may change the behavior of your CSS Code!';
+$lang['en'][42] = 'Remove last ;';
+$lang['en'][43] = 'Discard invalid properties';
+$lang['en'][44] = 'Only safe optimisations';
+$lang['en'][45] = 'Compress font-weight';
+$lang['en'][46] = 'Save comments';
+$lang['en'][47] = 'Do not change anything';
+$lang['en'][48] = 'Only separate selectors (split at ,)';
+$lang['en'][49] = 'Merge selectors with the same properties (fast)';
+$lang['en'][50] = 'Merge selectors intelligently (slow)';
+$lang['en'][51] = 'Preserve CSS';
+$lang['en'][52] = 'Save comments, hacks, etc. Most optimisations can *not* be applied if this is enabled.';
+$lang['en'][53] = 'None';
+$lang['en'][54] = 'Don\'t optimise';
+$lang['en'][55] = 'Safe optimisations';
+$lang['en'][56] = 'All optimisations';
+$lang['en'][57] = 'Add timestamp';
+$lang['en'][58] = 'Copy to clipboard';
+$lang['en'][59] = 'Back to top';
+$lang['en'][60] = 'Your browser doesn\'t support copy to clipboard.';
+$lang['en'][61] = 'For bugs and suggestions feel free to';
+$lang['en'][62] = 'contact me';
+$lang['en'][63] = 'Output CSS code as complete HTML document';
+$lang['en'][64] = 'Code';
+$lang['en'][65] = 'CSS to style CSS output';
+$lang['en'][66] = 'You need to go to about:config in your URL bar, select \'signed.applets.codebase_principal_support\' in the filter field, and set its value to true in order to use this feature; however, be aware that doing so increases security risks.';
+
+
+$lang['de'][0] = 'CSS Formatierer und Optimierer (basierend auf CSSTidy ';
+$lang['de'][1] = 'CSS Formatierer und Optimierer';
+$lang['de'][2] = '(basierend auf';
+$lang['de'][3] = '(Textversion)';
+$lang['de'][4] = 'Wichtiger Hinweis:';
+$lang['de'][6] = 'Der CSS Code sollte wohlgeformt sein. Der CSS Code wird <strong>nicht auf Gültigkeit überprüft</strong>. Um sicherzugehen dass dein Code valide ist, benutze den <a href="http://jigsaw.w3.org/css-validator/">W3C Validierungsservice</a>.';
+$lang['de'][7] = 'alle Kommentare werden entfernt';
+$lang['de'][8] = 'CSS Eingabe:';
+$lang['de'][9] = 'CSS-Code:';
+$lang['de'][10] = 'CSS von URL:';
+$lang['de'][11] = 'Code Layout:';
+$lang['de'][12] = 'Komprimierung&#160;(Code&#160;Layout):';
+$lang['de'][13] = 'Höchste (keine Lesbarkeit, niedrigste Größe)';
+$lang['de'][14] = 'Hoch (mittelmäßige Lesbarkeit, geringe Größe)';
+$lang['de'][15] = 'Standard (Kompromiss zwischen Lesbarkeit und Größe)';
+$lang['de'][16] = 'Niedrig (höhere Lesbarkeit)';
+$lang['de'][17] = 'Benutzerdefiniert (unten eingeben)';
+$lang['de'][18] = 'Benutzerdefinierte <a href="http://csstidy.sourceforge.net/templates.php">Vorlage</a>';
+$lang['de'][19] = 'Optionen';
+$lang['de'][20] = 'Selektoren sortieren (Vorsicht)';
+$lang['de'][21] = 'Eigenschaften sortieren';
+$lang['de'][22] = 'Selektoren umgruppieren';
+$lang['de'][23] = 'Shorthands optimieren';
+$lang['de'][24] = 'Farben komprimieren';
+$lang['de'][25] = 'Selektoren in Kleinbuchstaben';
+$lang['de'][26] = 'Groß-/Kleinschreibung für Eigenschaften';
+$lang['de'][27] = 'Kleinbuchstaben';
+$lang['de'][28] = 'Keine oder ungültige CSS Eingabe oder falsche URL!';
+$lang['de'][29] = 'Großbuchstaben';
+$lang['de'][30] = 'kleingeschriebene Elementnamen benötigt für XHTML';
+$lang['de'][31] = 'Unnötige Backslashes entfernen';
+$lang['de'][32] = '!important-Hack konvertieren';
+$lang['de'][33] = 'Als Datei ausgeben';
+$lang['de'][34] = 'Größere Komprimierung augrund von kleineren Neuezeile-Zeichen';
+$lang['de'][35] = 'CSS verarbeiten';
+$lang['de'][36] = 'Komprimierungsrate';
+$lang['de'][37] = 'Eingabe';
+$lang['de'][38] = 'Ausgabe';
+$lang['de'][39] = 'Sprache';
+$lang['de'][41] = 'Achtung: Dies könnte das Verhalten ihres CSS-Codes verändern!';
+$lang['de'][42] = 'Letztes ; entfernen';
+$lang['de'][43] = 'Ungültige Eigenschaften entfernen';
+$lang['de'][44] = 'Nur sichere Optimierungen';
+$lang['de'][45] = 'font-weight komprimieren';
+$lang['de'][46] = 'Kommentare beibehalten';
+$lang['de'][47] = 'Nichts ändern';
+$lang['de'][48] = 'Selektoren nur trennen (am Komma)';
+$lang['de'][49] = 'Selektoren mit gleichen Eigenschaften zusammenfassen (schnell)';
+$lang['de'][50] = 'Selektoren intelligent zusammenfassen (langsam!)';
+$lang['de'][51] = 'CSS erhalten';
+$lang['de'][52] = 'Kommentare, Hacks, etc. speichern. Viele Optimierungen sind dann aber nicht mehr möglich.';
+$lang['de'][53] = 'Keine';
+$lang['de'][54] = 'Nicht optimieren';
+$lang['de'][55] = 'Sichere Optimierungen';
+$lang['de'][56] = 'Alle Optimierungen';
+$lang['de'][57] = 'Zeitstempel hinzufügen';
+$lang['de'][58] = 'Copy to clipboard';
+$lang['de'][59] = 'Back to top';
+$lang['de'][60] = 'Your browser doesn\'t support copy to clipboard.';
+$lang['de'][61] = 'For bugs and suggestions feel free to';
+$lang['de'][62] = 'contact me';
+$lang['de'][63] = 'Output CSS code as complete HTML document';
+$lang['de'][64] = 'Code';
+$lang['de'][65] = 'CSS to style CSS output';
+$lang['de'][66] = 'You need to go to about:config in your URL bar, select \'signed.applets.codebase_principal_support\' in the filter field, and set its value to true in order to use this feature; however, be aware that doing so increases security risks.';
+
+
+$lang['fr'][0] = 'CSS Formatteur et Optimiseur (basé sur CSSTidy ';
+$lang['fr'][1] = 'CSS Formatteur et Optimiseur';
+$lang['fr'][2] = '(basé sur ';
+$lang['fr'][3] = '(Version texte)';
+$lang['fr'][4] = 'Note Importante&#160;:';
+$lang['fr'][6] = 'Votre code doit être valide. Ce n’est <strong>pas un validateur</strong> qui signale les erreurs dans votre code CSS. Pour être sûr que votre code est correct, utilisez le validateur&#160;: <a href="http://jigsaw.w3.org/css-validator/">W3C Validator</a>.';
+$lang['fr'][7] = 'tous les commentaires sont enlevés';
+$lang['fr'][8] = 'Champ CSS&#160;:';
+$lang['fr'][9] = 'Code CSS&#160;:';
+$lang['fr'][10] = 'CSS en provenance d’une URL&#160;:<br />';
+$lang['fr'][11] = 'Mise en page du code&#160;:';
+$lang['fr'][12] = 'Compression (mise en page du code)&#160;:';
+$lang['fr'][13] = 'La plus élevée (aucune lisibilité, taille minimale)';
+$lang['fr'][14] = 'Élevée (lisibilité modérée, petite taille)';
+$lang['fr'][15] = 'Normale (équilibre entre lisibilité et taille)';
+$lang['fr'][16] = 'Faible (lisibilité élevée)';
+$lang['fr'][17] = 'Sur mesure (entrer ci-dessous)';
+$lang['fr'][18] = '<a href="http://csstidy.sourceforge.net/templates.php">Gabarit</a> sur mesure';
+$lang['fr'][19] = 'Options';
+$lang['fr'][20] = 'Trier les sélecteurs (attention)';
+$lang['fr'][21] = 'Trier les propriétés';
+$lang['fr'][22] = 'Regrouper les sélecteurs';
+$lang['fr'][23] = 'Propriétés raccourcies';
+$lang['fr'][24] = 'Compresser les couleurs';
+$lang['fr'][25] = 'Sélecteurs en minuscules';
+$lang['fr'][26] = 'Case pour les propriétés&#160;:';
+$lang['fr'][27] = 'Minuscule';
+$lang['fr'][28] = 'CSS non valide ou URL incorrecte&#160;!';
+$lang['fr'][29] = 'Majuscule';
+$lang['fr'][30] = 'les noms des éléments en minuscules (indispensables pour XHTML)';
+$lang['fr'][31] = 'enlever les antislashs inutiles';
+$lang['fr'][32] = 'convertir !important-hack';
+$lang['fr'][33] = 'Sauver en tant que fichier';
+$lang['fr'][34] = 'Meilleure compression grâce aux caractères de saut de ligne plus petits (copier &#38; coller ne marche pas)';
+$lang['fr'][35] = 'Compresser la CSS';
+$lang['fr'][36] = 'Facteur de Compression';
+$lang['fr'][37] = 'Entrée';
+$lang['fr'][38] = 'Sortie';
+$lang['fr'][39] = 'Langue';
+$lang['fr'][41] = 'Attention&#160;: ceci peut changer le comportement de votre code CSS&#160;!';
+$lang['fr'][42] = 'Enlever le dernier ;';
+$lang['fr'][43] = 'Supprimer les propriétés non valide';
+$lang['fr'][44] = 'Seulement les optimisations sûres';
+$lang['fr'][45] = 'Compresser font-weight';
+$lang['fr'][46] = 'Sauvegarder les commentaires ';
+$lang['fr'][47] = 'Ne rien changer';
+$lang['fr'][48] = 'Sépare les sélecteurs (sépare au niveau de ,)';
+$lang['fr'][49] = 'Fusionne les sélecteurs avec les mêmes propriétés (rapide)';
+$lang['fr'][50] = 'Fusionne les sélecteurs intelligemment (lent)';
+$lang['fr'][51] = 'Préserver la CSS';
+$lang['fr'][52] = 'Sauvegarder les commentaires, hacks, etc. La plupart des optimisations ne peuvent *pas* être appliquées si cela est activé.';
+$lang['fr'][53] = 'Aucun';
+$lang['fr'][54] = 'Ne pas optimiser';
+$lang['fr'][55] = 'Optimisations sûres';
+$lang['fr'][56] = 'Toutes les optimisations';
+$lang['fr'][57] = 'Ajouter un timestamp';
+$lang['fr'][58] = 'Copier dans le presse-papiers';
+$lang['fr'][59] = 'Retour en haut';
+$lang['fr'][60] = 'Votre navigateur ne suporte pas la copie vers le presse-papiers.';
+$lang['fr'][61] = 'Pour signaler des bugs ou pour des suggestions,';
+$lang['fr'][62] = 'contactez-moi';
+$lang['fr'][63] = 'Sauver le code CSS comme document complet HTML';
+$lang['fr'][64] = 'Code';
+$lang['fr'][65] = 'CSS pour colorier la sortie CSS';
+$lang['fr'][66] = 'Vous devez aller dans about:config dans votre barre d’adresse, selectionner \'signed.applets.codebase_principal_support\' dans le champ Filtre et attribuez-lui la valeur \'true\' pour utiliser cette fonctionnalité; toutefois, soyez conscient que cela augmente les risques de sécurité.';
+
+
+$lang['zh'][0] = 'CSS整形與最佳化工具(使用 CSSTidy ';
+$lang['zh'][1] = 'CSS整形與最佳化工具';
+$lang['zh'][2] = '(使用';
+$lang['zh'][3] = '(純文字)';
+$lang['zh'][4] = '重要事項:';
+$lang['zh'][6] = '你的原始碼必須是良構的(well-formed). 這個工具<strong>沒有內建驗證器(validator)</strong>. 驗證器能夠指出你CSS原始碼裡的錯誤. 請使用 <a href="http://jigsaw.w3.org/css-validator/">W3C 驗證器</a>, 確保你的原始碼合乎規範.';
+$lang['zh'][7] = '所有註解都移除了';
+$lang['zh'][8] = 'CSS 輸入:';
+$lang['zh'][9] = 'CSS 原始碼:';
+$lang['zh'][10] = 'CSS 檔案網址(URL):';
+$lang['zh'][11] = '原始碼規劃:';
+$lang['zh'][12] = '壓縮程度(原始碼規劃):';
+$lang['zh'][13] = '最高 (沒有辦法讀, 檔案最小)';
+$lang['zh'][14] = '高 (適度的可讀性, 檔案小)';
+$lang['zh'][15] = '標準 (兼顧可讀性與檔案大小)';
+$lang['zh'][16] = '低 (注重可讀性)';
+$lang['zh'][17] = '自訂 (在下方設定)';
+$lang['zh'][18] = '自訂<a href="http://csstidy.sourceforge.net/templates.php">樣板</a>';
+$lang['zh'][19] = '選項';
+$lang['zh'][20] = '整理選擇符(請謹慎使用)';
+$lang['zh'][21] = '整理屬性';
+$lang['zh'][22] = '重組選擇符';
+$lang['zh'][23] = '速記法(shorthand)最佳化';
+$lang['zh'][24] = '壓縮色彩語法';
+$lang['zh'][25] = '改用小寫選擇符';
+$lang['zh'][26] = '屬性的字形:';
+$lang['zh'][27] = '小寫';
+$lang['zh'][28] = '沒有輸入CSS, 語法不符合規定, 或是網址錯誤!';
+$lang['zh'][29] = '大寫';
+$lang['zh'][30] = 'XHTML必須使用小寫的元素名稱';
+$lang['zh'][31] = '移除不必要的反斜線';
+$lang['zh'][32] = '轉換 !important-hack';
+$lang['zh'][33] = '輸出成檔案形式';
+$lang['zh'][34] = '由於比較少換行字元, 會有更大的壓縮比率(複製&#38;貼上沒有用)';
+$lang['zh'][35] = '執行';
+$lang['zh'][36] = '壓縮比率';
+$lang['zh'][37] = '輸入';
+$lang['zh'][38] = '輸出';
+$lang['zh'][39] = '語言';
+$lang['zh'][41] = '注意: 這或許會變更你CSS原始碼的行為!';
+$lang['zh'][42] = '除去最後一個分號';
+$lang['zh'][43] = '拋棄不符合規定的屬性';
+$lang['zh'][44] = '只安全地最佳化';
+$lang['zh'][45] = '壓縮 font-weight';
+$lang['zh'][46] = '保留註解';
+$lang['zh'][47] = '什麼都不要改';
+$lang['zh'][48] = '只分開原本用逗號分隔的選擇符';
+$lang['zh'][49] = '合併有相同屬性的選擇符(快速)';
+$lang['zh'][50] = '聰明地合併選擇符(慢速)';
+$lang['zh'][51] = '保護CSS';
+$lang['zh'][52] = '保留註解與 hack 等等. 如果啟用這個選項, 大多數的最佳化程序都不會執行.';
+$lang['zh'][53] = '不改變';
+$lang['zh'][54] = '不做最佳化';
+$lang['zh'][55] = '安全地最佳化';
+$lang['zh'][56] = '全部最佳化';
+$lang['zh'][57] = '加上時間戳記';
+$lang['zh'][58] = '复制到剪贴板';
+$lang['zh'][59] = '回到页面上方';
+$lang['zh'][60] = '你的浏览器不支持复制到剪贴板。';
+$lang['zh'][61] = '如果程序有错误或你有建议,欢迎';
+$lang['zh'][62] = '和我联系';
+$lang['zh'][63] = 'Output CSS code as complete HTML document';
+$lang['zh'][64] = '代码';
+$lang['zh'][65] = 'CSS to style CSS output';
+$lang['zh'][66] = 'You need to go to about:config in your URL bar, select \'signed.applets.codebase_principal_support\' in the filter field, and set its value to true in order to use this feature; however, be aware that doing so increases security risks.';
diff --git a/plugins/jetpack/modules/custom-css/csstidy/wordpress-standard.tpl b/plugins/jetpack/modules/custom-css/csstidy/wordpress-standard.tpl
new file mode 100644
index 00000000..9499e839
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/csstidy/wordpress-standard.tpl
@@ -0,0 +1,10 @@
+| {
+|| {
+| | |;
+|}|
+
+|
+}
+
+| ||
+|
diff --git a/plugins/jetpack/modules/custom-css/custom-css-4.7.php b/plugins/jetpack/modules/custom-css/custom-css-4.7.php
new file mode 100644
index 00000000..bb72caec
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css-4.7.php
@@ -0,0 +1,1165 @@
+<?php
+/**
+ * Alternate Custom CSS source for 4.7 compat.
+ *
+ * @since 4.4.2
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Class Jetpack_Custom_CSS_Enhancements
+ */
+class Jetpack_Custom_CSS_Enhancements {
+ /**
+ * Set up the actions and filters needed for our compatability layer on top of core's Custom CSS implementation.
+ */
+ public static function add_hooks() {
+ add_action( 'init', array( __CLASS__, 'init' ) );
+ add_action( 'admin_menu', array( __CLASS__, 'admin_menu' ) );
+ add_action( 'customize_controls_enqueue_scripts', array( __CLASS__, 'customize_controls_enqueue_scripts' ) );
+ add_action( 'customize_register', array( __CLASS__, 'customize_register' ) );
+ add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_cap' ), 20, 2 );
+ add_action( 'customize_preview_init', array( __CLASS__, 'customize_preview_init' ) );
+ add_filter( '_wp_post_revision_fields', array( __CLASS__, '_wp_post_revision_fields' ), 10, 2 );
+ add_action( 'load-revision.php', array( __CLASS__, 'load_revision_php' ) );
+
+ add_action( 'wp_enqueue_scripts', array( __CLASS__, 'wp_enqueue_scripts' ) );
+
+ // Handle Sass/LESS.
+ add_filter( 'customize_value_custom_css', array( __CLASS__, 'customize_value_custom_css' ), 10, 2 );
+ add_filter( 'customize_update_custom_css_post_content_args', array( __CLASS__, 'customize_update_custom_css_post_content_args' ), 10, 3 );
+ add_filter( 'update_custom_css_data', array( __CLASS__, 'update_custom_css_data' ), 10, 2 );
+
+ // Stuff for stripping out the theme's default stylesheet...
+ add_filter( 'stylesheet_uri', array( __CLASS__, 'style_filter' ) );
+ add_filter( 'safecss_skip_stylesheet', array( __CLASS__, 'preview_skip_stylesheet' ) );
+
+ // Stuff for overriding content width...
+ add_action( 'customize_preview_init', array( __CLASS__, 'preview_content_width' ) );
+ add_filter( 'jetpack_content_width', array( __CLASS__, 'jetpack_content_width' ) );
+ add_filter( 'editor_max_image_size', array( __CLASS__, 'editor_max_image_size' ), 10, 3 );
+ add_action( 'template_redirect', array( __CLASS__, 'set_content_width' ) );
+ add_action( 'admin_init', array( __CLASS__, 'set_content_width' ) );
+
+ // Stuff?
+ }
+
+ /**
+ * Things that we do on init.
+ */
+ public static function init() {
+ $min = '.min';
+ if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
+ $min = '';
+ }
+
+ wp_register_style( 'jetpack-codemirror', plugins_url( 'custom-css/css/codemirror.css', __FILE__ ), array(), '20120905' );
+ wp_register_style( 'jetpack-customizer-css', plugins_url( 'custom-css/css/customizer-control.css', __FILE__ ), array(), '20140728' );
+ wp_register_script( 'jetpack-codemirror', plugins_url( 'custom-css/js/codemirror.min.js', __FILE__ ), array(), '3.16', true );
+ $src = Jetpack::get_file_url_for_environment(
+ '_inc/build/custom-css/custom-css/js/core-customizer-css.core-4.9.min.js',
+ 'modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js'
+ );
+ wp_register_script( 'jetpack-customizer-css', $src, array( 'customize-controls', 'underscore' ), JETPACK__VERSION, true );
+
+ wp_register_script(
+ 'jetpack-customizer-css-preview',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/custom-css/custom-css/js/core-customizer-css-preview.min.js',
+ 'modules/custom-css/custom-css/js/core-customizer-css-preview.js'
+ ),
+ array( 'customize-selective-refresh' ),
+ JETPACK__VERSION,
+ true
+ );
+
+ remove_action( 'wp_head', 'wp_custom_css_cb', 11 ); // 4.7.0 had it at 11, 4.7.1 moved it to 101.
+ remove_action( 'wp_head', 'wp_custom_css_cb', 101 );
+ add_action( 'wp_head', array( __CLASS__, 'wp_custom_css_cb' ), 101 );
+
+ if ( isset( $_GET['custom-css'] ) ) {
+ self::print_linked_custom_css();
+ }
+ }
+
+ /**
+ * Things that we do on init when the Customize Preview is loading.
+ */
+ public static function customize_preview_init() {
+ add_filter( 'wp_get_custom_css', array( __CLASS__, 'customize_preview_wp_get_custom_css' ) );
+ }
+
+ /**
+ * Print the current Custom CSS. This is for linking instead of printing directly.
+ */
+ public static function print_linked_custom_css() {
+ header( 'Content-type: text/css' );
+ header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + YEAR_IN_SECONDS ) . ' GMT' );
+ echo wp_get_custom_css();
+ exit;
+ }
+
+ /**
+ * Re-map the Edit CSS capability.
+ *
+ * Core, by default, restricts this to users that have `unfiltered_html` which
+ * would make the feature unusable in multi-site by non-super-admins, due to Core
+ * not shipping any solid sanitization.
+ *
+ * We're expanding who can use it, and then conditionally applying CSSTidy
+ * sanitization to users that do not have the `unfiltered_html` capability.
+ *
+ * @param array $caps Returns the user's actual capabilities.
+ * @param string $cap Capability name.
+ *
+ * @return array $caps
+ */
+ public static function map_meta_cap( $caps, $cap ) {
+ if ( 'edit_css' === $cap ) {
+ $caps = array( 'edit_theme_options' );
+ }
+ return $caps;
+ }
+
+ /**
+ * Handle our admin menu item and legacy page declaration.
+ */
+ public static function admin_menu() {
+ // Add in our legacy page to support old bookmarks and such.
+ add_submenu_page( null, __( 'CSS', 'jetpack' ), __( 'Edit CSS', 'jetpack' ), 'edit_theme_options', 'editcss', array( __CLASS__, 'admin_page' ) );
+
+ // Add in our new page slug that will redirect to the customizer.
+ $hook = add_theme_page( __( 'CSS', 'jetpack' ), __( 'Edit CSS', 'jetpack' ), 'edit_theme_options', 'editcss-customizer-redirect', array( __CLASS__, 'admin_page' ) );
+ add_action( "load-{$hook}", array( __CLASS__, 'customizer_redirect' ) );
+ }
+
+ /**
+ * Handle the redirect for the customizer. This is necessary because
+ * we can't directly add customizer links to the admin menu.
+ *
+ * There is a core patch in trac that would make this unnecessary.
+ *
+ * @link https://core.trac.wordpress.org/ticket/39050
+ */
+ public static function customizer_redirect() {
+ wp_safe_redirect( self::customizer_link( array(
+ 'return_url' => wp_get_referer(),
+ ) ) );
+ exit;
+ }
+
+ /**
+ * Shows Preprocessor code in the Revisions screen, and ensures that post_content_filtered
+ * is maintained on revisions
+ *
+ * @param array $fields Post fields pertinent to revisions.
+ * @param array $post A post array being processed for insertion as a post revision.
+ *
+ * @return array $fields Modified array to include post_content_filtered.
+ */
+ public static function _wp_post_revision_fields( $fields, $post ) {
+ // None of the fields in $post are required to be passed in this filter.
+ if ( ! isset( $post['post_type'], $post['ID'] ) ) {
+ return $fields;
+ }
+
+ // If we're passed in a revision, go get the main post instead.
+ if ( 'revision' === $post['post_type'] ) {
+ $main_post_id = wp_is_post_revision( $post['ID'] );
+ $post = get_post( $main_post_id, ARRAY_A );
+ }
+ if ( 'custom_css' === $post['post_type'] ) {
+ $fields['post_content'] = __( 'CSS', 'jetpack' );
+ $fields['post_content_filtered'] = __( 'Preprocessor', 'jetpack' );
+ }
+ return $fields;
+ }
+
+ /**
+ * Get the published custom CSS post.
+ *
+ * @param string $stylesheet Optional. A theme object stylesheet name. Defaults to the current theme.
+ * @return WP_Post|null
+ */
+ public static function get_css_post( $stylesheet = '' ) {
+ return wp_get_custom_css_post( $stylesheet );
+ }
+
+ /**
+ * Override Core's `wp_custom_css_cb` method to provide linking to custom css.
+ */
+ public static function wp_custom_css_cb() {
+ $styles = wp_get_custom_css();
+ if ( strlen( $styles ) > 2000 && ! is_customize_preview() ) :
+ // Add a cache buster to the url.
+ $url = home_url( '/' );
+ $url = add_query_arg( 'custom-css', substr( md5( $styles ), -10 ), $url );
+ ?>
+ <link rel="stylesheet" type="text/css" id="wp-custom-css" href="<?php echo esc_url( $url ); ?>" />
+ <?php elseif ( $styles || is_customize_preview() ) : ?>
+ <style type="text/css" id="wp-custom-css">
+ <?php echo strip_tags( $styles ); // Note that esc_html() cannot be used because `div &gt; span` is not interpreted properly. ?>
+ </style>
+ <?php endif;
+ }
+
+ /**
+ * Get the ID of a Custom CSS post tying to a given stylesheet.
+ *
+ * @param string $stylesheet Stylesheet name.
+ *
+ * @return int $post_id Post ID.
+ */
+ public static function post_id( $stylesheet = '' ) {
+ $post = self::get_css_post( $stylesheet );
+ if ( $post instanceof WP_Post ) {
+ return $post->ID;
+ }
+ return 0;
+ }
+
+ /**
+ * Partial for use in the Customizer.
+ */
+ public static function echo_custom_css_partial() {
+ echo wp_get_custom_css();
+ }
+
+ /**
+ * Admin page!
+ *
+ * This currently has two main uses -- firstly to display the css for an inactive
+ * theme if there are no revisions attached it to a legacy bug, and secondly to
+ * handle folks that have bookmarkes in their browser going to the old page for
+ * managing Custom CSS in Jetpack.
+ *
+ * If we ever add back in a non-Customizer CSS editor, this would be the place.
+ */
+ public static function admin_page() {
+ $post = null;
+ $stylesheet = null;
+ if ( isset( $_GET['id'] ) ) {
+ $post_id = absint( $_GET['id'] );
+ $post = get_post( $post_id );
+ if ( $post instanceof WP_Post && 'custom_css' === $post->post_type ) {
+ $stylesheet = $post->post_title;
+ }
+ }
+ ?>
+ <div class="wrap">
+ <?php self::revisions_switcher_box( $stylesheet ); ?>
+ <h1>
+ <?php
+ if ( $post ) {
+ printf( 'Custom CSS for &#8220;%1$s&#8221;', wp_get_theme( $stylesheet )->Name );
+ } else {
+ esc_html_e( 'Custom CSS', 'jetpack' );
+ }
+ if ( current_user_can( 'customize' ) ) {
+ printf(
+ ' <a class="page-title-action hide-if-no-customize" href="%1$s">%2$s</a>',
+ esc_url( self::customizer_link() ),
+ esc_html__( 'Manage with Live Preview', 'jetpack' )
+ );
+ }
+ ?>
+ </h1>
+ <p><?php esc_html_e( 'Custom CSS is now managed in the Customizer.', 'jetpack' ); ?></p>
+ <?php if ( $post ) : ?>
+ <div class="revisions">
+ <h3><?php esc_html_e( 'CSS', 'jetpack' ); ?></h3>
+ <textarea class="widefat" readonly><?php echo esc_textarea( $post->post_content ); ?></textarea>
+ <?php if ( $post->post_content_filtered ) : ?>
+ <h3><?php esc_html_e( 'Preprocessor', 'jetpack' ); ?></h3>
+ <textarea class="widefat" readonly><?php echo esc_textarea( $post->post_content_filtered ); ?></textarea>
+ <?php endif; ?>
+ </div>
+ <?php endif; ?>
+ </div>
+
+ <style>
+ .other-themes-wrap {
+ float: right;
+ background-color: #fff;
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ padding: 5px 10px;
+ margin-bottom: 10px;
+ }
+ .other-themes-wrap label {
+ display: block;
+ margin-bottom: 10px;
+ }
+ .other-themes-wrap select {
+ float: left;
+ width: 77%;
+ }
+ .other-themes-wrap button {
+ float: right;
+ width: 20%;
+ }
+ .revisions {
+ clear: both;
+ }
+ .revisions textarea {
+ min-height: 300px;
+ background: #fff;
+ }
+ </style>
+ <script>
+ (function($){
+ var $switcher = $('.other-themes-wrap');
+ $switcher.find('button').on('click', function(e){
+ e.preventDefault();
+ if ( $switcher.find('select').val() ) {
+ window.location.href = $switcher.find('select').val();
+ }
+ });
+ })(jQuery);
+ </script>
+ <?php
+ }
+
+ /**
+ * Build the URL to deep link to the Customizer.
+ *
+ * You can modify the return url via $args.
+ *
+ * @param array $args Array of parameters.
+ * @return string
+ */
+ public static function customizer_link( $args = array() ) {
+ $args = wp_parse_args( $args, array(
+ 'return_url' => urlencode( wp_unslash( $_SERVER['REQUEST_URI'] ) ),
+ ) );
+
+ return add_query_arg(
+ array(
+ array(
+ 'autofocus' => array(
+ 'section' => 'custom_css',
+ ),
+ ),
+ 'return' => $args['return_url'],
+ ),
+ admin_url( 'customize.php' )
+ );
+ }
+
+ /**
+ * Handle the enqueueing and localizing for scripts to be used in the Customizer.
+ */
+ public static function customize_controls_enqueue_scripts() {
+ wp_enqueue_style( 'jetpack-customizer-css' );
+ wp_enqueue_script( 'jetpack-customizer-css' );
+
+ $content_help = __( 'Set a different content width for full size images.', 'jetpack' );
+ if ( ! empty( $GLOBALS['content_width'] ) ) {
+ $content_help .= sprintf(
+ _n( ' The default content width for the <strong>%1$s</strong> theme is %2$d pixel.', ' The default content width for the <strong>%1$s</strong> theme is %2$d pixels.', intval( $GLOBALS['content_width'] ), 'jetpack' ),
+ wp_get_theme()->Name,
+ intval( $GLOBALS['content_width'] )
+ );
+ }
+
+ wp_localize_script( 'jetpack-customizer-css', '_jp_css_settings', array(
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ 'useRichEditor' => ! jetpack_is_mobile() && apply_filters( 'safecss_use_ace', true ),
+ 'areThereCssRevisions' => self::are_there_css_revisions(),
+ 'revisionsUrl' => self::get_revisions_url(),
+ 'cssHelpUrl' => '//en.support.wordpress.com/custom-design/editing-css/',
+ 'l10n' => array(
+ 'mode' => __( 'Start Fresh', 'jetpack' ),
+ 'mobile' => __( 'On Mobile', 'jetpack' ),
+ 'contentWidth' => $content_help,
+ 'revisions' => _x( 'See full history', 'Toolbar button to see full CSS revision history', 'jetpack' ),
+ 'css_help_title' => _x( 'Help', 'Toolbar button to get help with custom CSS', 'jetpack' ),
+ ),
+ ));
+ }
+
+ /**
+ * Check whether there are CSS Revisions for a given theme.
+ *
+ * Going forward, there should always be, but this was necessitated
+ * early on by https://core.trac.wordpress.org/ticket/30854
+ *
+ * @param string $stylesheet Stylesheet name.
+ *
+ * @return bool|null|WP_Post
+ */
+ public static function are_there_css_revisions( $stylesheet = '' ) {
+ $post = wp_get_custom_css_post( $stylesheet );
+ if ( empty( $post ) ) {
+ return $post;
+ }
+ return (bool) wp_get_post_revisions( $post );
+ }
+
+ /**
+ * Core doesn't have a function to get the revisions url for a given post ID.
+ *
+ * @param string $stylesheet Stylesheet name.
+ *
+ * @return null|string|void
+ */
+ public static function get_revisions_url( $stylesheet = '' ) {
+ $post = wp_get_custom_css_post( $stylesheet );
+
+ // If we have any currently saved customizations...
+ if ( $post instanceof WP_Post ) {
+ $revisions = wp_get_post_revisions( $post->ID, array( 'posts_per_page' => 1 ) );
+ if ( empty( $revisions ) || is_wp_error( $revisions ) ) {
+ return admin_url( 'themes.php?page=editcss' );
+ }
+ $revision = reset( $revisions );
+ return get_edit_post_link( $revision->ID );
+ }
+
+ return admin_url( 'themes.php?page=editcss' );
+ }
+
+ /**
+ * Get a map of all theme names and theme stylesheets for mapping stuff.
+ *
+ * @return array
+ */
+ public static function get_themes() {
+ $themes = wp_get_themes( array( 'errors' => null ) );
+ $all = array();
+ foreach ( $themes as $theme ) {
+ $all[ $theme->name ] = $theme->stylesheet;
+ }
+ return $all;
+ }
+
+ /**
+ * When we need to get all themes that have Custom CSS saved.
+ *
+ * @return array
+ */
+ public static function get_all_themes_with_custom_css() {
+ $themes = self::get_themes();
+ $custom_css = get_posts( array(
+ 'post_type' => 'custom_css',
+ 'post_status' => get_post_stati(),
+ 'number' => -1,
+ 'order' => 'DESC',
+ 'orderby' => 'modified',
+ ) );
+ $return = array();
+
+ foreach ( $custom_css as $post ) {
+ $stylesheet = $post->post_title;
+ $label = array_search( $stylesheet, $themes );
+
+ if ( ! $label ) {
+ continue;
+ }
+
+ $return[ $stylesheet ] = array(
+ 'label' => $label,
+ 'post' => $post,
+ );
+ }
+
+ return $return;
+ }
+
+ /**
+ * Handle the enqueueing of scripts for customize previews.
+ */
+ public static function wp_enqueue_scripts() {
+ if ( is_customize_preview() ) {
+ wp_enqueue_script( 'jetpack-customizer-css-preview' );
+ wp_localize_script( 'jetpack-customizer-css-preview', 'jpCustomizerCssPreview', array(
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ 'preprocessors' => apply_filters( 'jetpack_custom_css_preprocessors', array() ),
+ ));
+ }
+ }
+
+ /**
+ * Sanitize the CSS for users without `unfiltered_html`.
+ *
+ * @param string $css Input CSS.
+ * @param array $args Array of CSS options.
+ *
+ * @return mixed|string
+ */
+ public static function sanitize_css( $css, $args = array() ) {
+ $args = wp_parse_args( $args, array(
+ 'force' => false,
+ 'preprocessor' => null,
+ ) );
+
+ if ( $args['force'] || ! current_user_can( 'unfiltered_html' ) ) {
+
+ $warnings = array();
+
+ safecss_class();
+ $csstidy = new csstidy();
+ $csstidy->optimise = new safecss( $csstidy );
+
+ $csstidy->set_cfg( 'remove_bslash', false );
+ $csstidy->set_cfg( 'compress_colors', false );
+ $csstidy->set_cfg( 'compress_font-weight', false );
+ $csstidy->set_cfg( 'optimise_shorthands', 0 );
+ $csstidy->set_cfg( 'remove_last_;', false );
+ $csstidy->set_cfg( 'case_properties', false );
+ $csstidy->set_cfg( 'discard_invalid_properties', true );
+ $csstidy->set_cfg( 'css_level', 'CSS3.0' );
+ $csstidy->set_cfg( 'preserve_css', true );
+ $csstidy->set_cfg( 'template', dirname( __FILE__ ) . '/csstidy/wordpress-standard.tpl' );
+
+ // Test for some preg_replace stuff.
+ {
+ $prev = $css;
+ $css = preg_replace( '/\\\\([0-9a-fA-F]{4})/', '\\\\\\\\$1', $css );
+ // prevent content: '\3434' from turning into '\\3434'.
+ $css = str_replace( array( '\'\\\\', '"\\\\' ), array( '\'\\', '"\\' ), $css );
+ if ( $css !== $prev ) {
+ $warnings[] = 'preg_replace found stuff';
+ }
+ }
+
+ // Some people put weird stuff in their CSS, KSES tends to be greedy.
+ $css = str_replace( '<=', '&lt;=', $css );
+
+ // Test for some kses stuff.
+ {
+ $prev = $css;
+ // Why KSES instead of strip_tags? Who knows?
+ $css = wp_kses_split( $css, array(), array() );
+ $css = str_replace( '&gt;', '>', $css ); // kses replaces lone '>' with &gt;
+ // Why both KSES and strip_tags? Because we just added some '>'.
+ $css = strip_tags( $css );
+
+ if ( $css != $prev ) {
+ $warnings[] = 'kses found stuff';
+ }
+ }
+
+ // if we're not using a preprocessor.
+ if ( ! $args['preprocessor'] ) {
+
+ /** This action is documented in modules/custom-css/custom-css.php */
+ do_action( 'safecss_parse_pre', $csstidy, $css, $args );
+
+ $csstidy->parse( $css );
+
+ /** This action is documented in modules/custom-css/custom-css.php */
+ do_action( 'safecss_parse_post', $csstidy, $warnings, $args );
+
+ $css = $csstidy->print->plain();
+ }
+ }
+ return $css;
+ }
+
+ /**
+ * Override $content_width in customizer previews.
+ */
+ public static function preview_content_width() {
+ global $wp_customize;
+ if ( ! is_customize_preview() ) {
+ return;
+ }
+
+ $setting = $wp_customize->get_setting( 'jetpack_custom_css[content_width]' );
+ if ( ! $setting ) {
+ return;
+ }
+
+ $customized_content_width = (int) $setting->post_value();
+ if ( ! empty( $customized_content_width ) ) {
+ $GLOBALS['content_width'] = $customized_content_width;
+ }
+ }
+
+ /**
+ * Filter the current theme's stylesheet for potentially nullifying it.
+ *
+ * @param string $current Stylesheet URI for the current theme/child theme.
+ *
+ * @return mixed|void
+ */
+ static function style_filter( $current ) {
+ if ( is_admin() ) {
+ return $current;
+ } elseif ( self::is_freetrial() && ( ! self::is_preview() || ! current_user_can( 'switch_themes' ) ) ) {
+ return $current;
+ } elseif ( self::skip_stylesheet() ) {
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ return apply_filters( 'safecss_style_filter_url', plugins_url( 'custom-css/css/blank.css', __FILE__ ) );
+ }
+
+ return $current;
+ }
+
+ /**
+ * Determine whether or not we should have the theme skip its main stylesheet.
+ *
+ * @return mixed The truthiness of this value determines whether the stylesheet should be skipped.
+ */
+ static function skip_stylesheet() {
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $skip_stylesheet = apply_filters( 'safecss_skip_stylesheet', null );
+ if ( ! is_null( $skip_stylesheet ) ) {
+ return $skip_stylesheet;
+ }
+
+ $jetpack_custom_css = get_theme_mod( 'jetpack_custom_css', array() );
+ if ( isset( $jetpack_custom_css['replace'] ) ) {
+ return $jetpack_custom_css['replace'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Override $content_width in customizer previews.
+ *
+ * Runs on `safecss_skip_stylesheet` filter.
+ *
+ * @param bool $skip_value Should the stylesheet be skipped.
+ *
+ * @return null|bool
+ */
+ public static function preview_skip_stylesheet( $skip_value ) {
+ global $wp_customize;
+ if ( ! is_customize_preview() ) {
+ return $skip_value;
+ }
+
+ $setting = $wp_customize->get_setting( 'jetpack_custom_css[replace]' );
+ if ( ! $setting ) {
+ return $skip_value;
+ }
+
+ $customized_replace = $setting->post_value();
+ if ( null !== $customized_replace ) {
+ return $customized_replace;
+ }
+
+ return $skip_value;
+ }
+
+ /**
+ * Add Custom CSS section and controls.
+ *
+ * @param WP_Customize_Manager $wp_customize WP_Customize_Manager instance.
+ */
+ public static function customize_register( $wp_customize ) {
+
+ /**
+ * SETTINGS.
+ */
+
+ $wp_customize->add_setting( 'jetpack_custom_css[preprocessor]', array(
+ 'default' => '',
+ 'transport' => 'postMessage',
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_preprocessor' ),
+ ) );
+
+ $wp_customize->add_setting( 'jetpack_custom_css[replace]', array(
+ 'default' => false,
+ 'transport' => 'refresh',
+ ) );
+
+ $wp_customize->add_setting( 'jetpack_custom_css[content_width]', array(
+ 'default' => '',
+ 'transport' => 'refresh',
+ 'sanitize_callback' => array( __CLASS__, 'intval_base10' ),
+ ) );
+
+ // Add custom sanitization to the core css customizer setting.
+ foreach ( $wp_customize->settings() as $setting ) {
+ if ( $setting instanceof WP_Customize_Custom_CSS_Setting ) {
+ add_filter( "customize_sanitize_{$setting->id}", array( __CLASS__, 'sanitize_css_callback' ), 10, 2 );
+ }
+ }
+
+ /**
+ * CONTROLS.
+ */
+
+ // Overwrite or Tweak the Core Control.
+ $core_custom_css = $wp_customize->get_control( 'custom_css' );
+ if ( $core_custom_css ) {
+ if ( $core_custom_css instanceof WP_Customize_Code_Editor_Control ) {
+ // In WP 4.9, we let the Core CodeMirror control keep running the show, but hook into it to tweak stuff.
+ $types = array(
+ 'default' => 'text/css',
+ 'less' => 'text/x-less',
+ 'sass' => 'text/x-scss',
+ );
+ $preprocessor = $wp_customize->get_setting( 'jetpack_custom_css[preprocessor]' )->value();
+ if ( isset( $types[ $preprocessor ] ) ) {
+ $core_custom_css->code_type = $types[ $preprocessor ];
+ }
+ } else {
+ // Core < 4.9 Fallback
+ $core_custom_css->type = 'jetpackCss';
+ }
+ }
+
+ $wp_customize->selective_refresh->add_partial( 'custom_css', array(
+ 'type' => 'custom_css',
+ 'selector' => '#wp-custom-css',
+ 'container_inclusive' => false,
+ 'fallback_refresh' => false,
+ 'settings' => array(
+ 'custom_css[' . $wp_customize->get_stylesheet() . ']',
+ 'jetpack_custom_css[preprocessor]',
+ ),
+ 'render_callback' => array( __CLASS__, 'echo_custom_css_partial' ),
+ ) );
+
+ $wp_customize->add_control( 'wpcom_custom_css_content_width_control', array(
+ 'type' => 'text',
+ 'label' => __( 'Media Width', 'jetpack' ),
+ 'section' => 'custom_css',
+ 'settings' => 'jetpack_custom_css[content_width]',
+ ) );
+
+ $wp_customize->add_control( 'jetpack_css_mode_control', array(
+ 'type' => 'checkbox',
+ 'label' => __( 'Don\'t use the theme\'s original CSS.', 'jetpack' ),
+ 'section' => 'custom_css',
+ 'settings' => 'jetpack_custom_css[replace]',
+ ) );
+
+ /**
+ * An action to grab on to if another Jetpack Module would like to add its own controls.
+ *
+ * @module custom-css
+ *
+ * @since 4.4.2
+ *
+ * @param $wp_customize The WP_Customize object.
+ */
+ do_action( 'jetpack_custom_css_customizer_controls', $wp_customize );
+
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
+ if ( ! empty( $preprocessors ) ) {
+ $preprocessor_choices = array(
+ '' => __( 'None', 'jetpack' ),
+ );
+
+ foreach ( $preprocessors as $preprocessor_key => $processor ) {
+ $preprocessor_choices[ $preprocessor_key ] = $processor['name'];
+ }
+
+ $wp_customize->add_control( 'jetpack_css_preprocessors_control', array(
+ 'type' => 'select',
+ 'choices' => $preprocessor_choices,
+ 'label' => __( 'Preprocessor', 'jetpack' ),
+ 'section' => 'custom_css',
+ 'settings' => 'jetpack_custom_css[preprocessor]',
+ ) );
+ }
+
+ }
+
+ /**
+ * The callback to handle sanitizing the CSS. Takes different arguments, hence the proxy function.
+ *
+ * @param mixed $css Value of the setting.
+ * @param WP_Customize_Setting $setting WP_Customize_Setting instance.
+ *
+ * @return mixed|string
+ */
+ public static function sanitize_css_callback( $css, $setting ) {
+ global $wp_customize;
+ return self::sanitize_css( $css, array(
+ 'preprocessor' => $wp_customize->get_setting( 'jetpack_custom_css[preprocessor]' )->value(),
+ ) );
+ }
+
+ /**
+ * Flesh out for wpcom.
+ *
+ * @todo
+ *
+ * @return bool
+ */
+ public static function is_freetrial() {
+ return false;
+ }
+
+ /**
+ * Flesh out for wpcom.
+ *
+ * @todo
+ *
+ * @return bool
+ */
+ public static function is_preview() {
+ return false;
+ }
+
+ /**
+ * Output the custom css for customize preview.
+ *
+ * @param string $css Custom CSS content.
+ *
+ * @return mixed
+ */
+ public static function customize_preview_wp_get_custom_css( $css ) {
+ global $wp_customize;
+
+ $preprocessor = $wp_customize->get_setting( 'jetpack_custom_css[preprocessor]' )->value();
+
+ // If it's empty, just return.
+ if ( empty( $preprocessor ) ) {
+ return $css;
+ }
+
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
+ if ( isset( $preprocessors[ $preprocessor ] ) ) {
+ return call_user_func( $preprocessors[ $preprocessor ]['callback'], $css );
+ }
+
+ return $css;
+ }
+
+ /**
+ * Add CSS preprocessing to our CSS if it is supported.
+ *
+ * @param mixed $css Value of the setting.
+ * @param WP_Customize_Setting $setting WP_Customize_Setting instance.
+ *
+ * @return string
+ */
+ public static function customize_value_custom_css( $css, $setting ) {
+ // Find the current preprocessor.
+ $jetpack_custom_css = get_theme_mod( 'jetpack_custom_css', array() );
+ if ( isset( $jetpack_custom_css['preprocessor'] ) ) {
+ $preprocessor = $jetpack_custom_css['preprocessor'];
+ }
+
+ // If it's not supported, just return.
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
+ if ( ! isset( $preprocessors[ $preprocessor ] ) ) {
+ return $css;
+ }
+
+ // Swap it for the `post_content_filtered` instead.
+ $post = wp_get_custom_css_post( $setting->stylesheet );
+ if ( $post && ! empty( $post->post_content_filtered ) ) {
+ $css = $post->post_content_filtered;
+ }
+
+ return $css;
+ }
+
+ /**
+ * Store the original pre-processed CSS in `post_content_filtered`
+ * and then store processed CSS in `post_content`.
+ *
+ * @param array $args Content post args.
+ * @param string $css Original CSS being updated.
+ * @param WP_Customize_Custom_CSS_Setting $setting Custom CSS Setting.
+ *
+ * @return mixed
+ */
+ public static function customize_update_custom_css_post_content_args( $args, $css, $setting ) {
+ // Find the current preprocessor.
+ $jetpack_custom_css = get_theme_mod( 'jetpack_custom_css', array() );
+ if ( empty( $jetpack_custom_css['preprocessor'] ) ) {
+ return $args;
+ }
+
+ $preprocessor = $jetpack_custom_css['preprocessor'];
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
+
+ // If it's empty, just return.
+ if ( empty( $preprocessor ) ) {
+ return $args;
+ }
+
+ if ( isset( $preprocessors[ $preprocessor ] ) ) {
+ $args['post_content_filtered'] = $css;
+ $args['post_content'] = call_user_func( $preprocessors[ $preprocessor ]['callback'], $css );
+ }
+
+ return $args;
+ }
+
+ /**
+ * Filter to handle the processing of preprocessed css on save.
+ *
+ * @param array $args Custom CSS options.
+ * @param string $stylesheet Original CSS to be updated.
+ *
+ * @return mixed
+ */
+ public static function update_custom_css_data( $args, $stylesheet ) {
+ // Find the current preprocessor.
+ $jetpack_custom_css = get_theme_mod( 'jetpack_custom_css', array() );
+ if ( empty( $jetpack_custom_css['preprocessor'] ) ) {
+ return $args;
+ }
+
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
+ $preprocessor = $jetpack_custom_css['preprocessor'];
+
+ // If we have a preprocessor specified ...
+ if ( isset( $preprocessors[ $preprocessor ] ) ) {
+ // And no other preprocessor has run ...
+ if ( empty( $args['preprocessed'] ) ) {
+ $args['preprocessed'] = $args['css'];
+ $args['css'] = call_user_func( $preprocessors[ $preprocessor ]['callback'], $args['css'] );
+ } else {
+ trigger_error( 'Jetpack CSS Preprocessor specified, but something else has already modified the argument.', E_USER_WARNING );
+ }
+ }
+
+ return $args;
+ }
+
+ /**
+ * When on the edit screen, make sure the custom content width
+ * setting is applied to the large image size.
+ *
+ * @param array $dims Array of image dimensions (width and height).
+ * @param string $size Size of the resulting image.
+ * @param null $context Context the image is being resized for. `edit` or `display`.
+ *
+ * @return array
+ */
+ static function editor_max_image_size( $dims, $size = 'medium', $context = null ) {
+ list( $width, $height ) = $dims;
+
+ if ( 'large' === $size && 'edit' === $context ) {
+ $width = Jetpack::get_content_width();
+ }
+
+ return array( $width, $height );
+ }
+
+ /**
+ * Override the content_width with a custom value if one is set.
+ *
+ * @param int $content_width Content Width value to be updated.
+ *
+ * @return int
+ */
+ static function jetpack_content_width( $content_width ) {
+ $custom_content_width = 0;
+
+ $jetpack_custom_css = get_theme_mod( 'jetpack_custom_css', array() );
+ if ( isset( $jetpack_custom_css['content_width'] ) ) {
+ $custom_content_width = $jetpack_custom_css['content_width'];
+ }
+
+ if ( $custom_content_width > 0 ) {
+ return $custom_content_width;
+ }
+
+ return $content_width;
+ }
+
+ /**
+ * Currently this filter function gets called on
+ * 'template_redirect' action and
+ * 'admin_init' action
+ */
+ static function set_content_width() {
+ // Don't apply this filter on the Edit CSS page.
+ if ( isset( $_GET['page'] ) && 'editcss' === $_GET['page'] && is_admin() ) {
+ return;
+ }
+
+ $GLOBALS['content_width'] = Jetpack::get_content_width();
+ }
+
+ /**
+ * Make sure the preprocessor we're saving is one we know about.
+ *
+ * @param string $preprocessor The preprocessor to sanitize.
+ *
+ * @return null|string
+ */
+ public static function sanitize_preprocessor( $preprocessor ) {
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
+ if ( empty( $preprocessor ) || array_key_exists( $preprocessor, $preprocessors ) ) {
+ return $preprocessor;
+ }
+ return null;
+ }
+
+ /**
+ * Get the base10 intval.
+ *
+ * This is used as a setting's sanitize_callback; we can't use just plain
+ * intval because the second argument is not what intval() expects.
+ *
+ * @access public
+ *
+ * @param mixed $value Number to convert.
+ * @return int Integer.
+ */
+ public static function intval_base10( $value ) {
+ return intval( $value, 10 );
+ }
+
+ /**
+ * Add a footer action on revision.php to print some customizations for the theme switcher.
+ */
+ public static function load_revision_php() {
+ add_action( 'admin_footer', array( __CLASS__, 'revision_admin_footer' ) );
+ }
+
+ /**
+ * Print the theme switcher on revision.php and move it into place.
+ */
+ public static function revision_admin_footer() {
+ $post = get_post();
+ if ( 'custom_css' !== $post->post_type ) {
+ return;
+ }
+ $stylesheet = $post->post_title;
+ ?>
+<script type="text/html" id="tmpl-other-themes-switcher">
+ <?php self::revisions_switcher_box( $stylesheet ); ?>
+</script>
+<style>
+.other-themes-wrap {
+ float: right;
+ background-color: #fff;
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ padding: 5px 10px;
+ margin-bottom: 10px;
+}
+.other-themes-wrap label {
+ display: block;
+ margin-bottom: 10px;
+}
+.other-themes-wrap select {
+ float: left;
+ width: 77%;
+}
+.other-themes-wrap button {
+ float: right;
+ width: 20%;
+}
+.revisions {
+ clear: both;
+}
+/* Hide the back-to-post link */
+.long-header + a {
+ display: none;
+}
+</style>
+<script>
+(function($){
+ var switcher = $('#tmpl-other-themes-switcher').html(),
+ qty = $( switcher ).find('select option').length,
+ $switcher;
+
+ if ( qty >= 3 ) {
+ $('h1.long-header').before( switcher );
+ $switcher = $('.other-themes-wrap');
+ $switcher.find('button').on('click', function(e){
+ e.preventDefault();
+ if ( $switcher.find('select').val() ) {
+ window.location.href = $switcher.find('select').val();
+ }
+ })
+ }
+})(jQuery);
+</script>
+ <?php
+ }
+
+ /**
+ * The HTML for the theme revision switcher box.
+ *
+ * @param string $stylesheet Stylesheet name.
+ */
+ public static function revisions_switcher_box( $stylesheet = '' ) {
+ $themes = self::get_all_themes_with_custom_css();
+ ?>
+ <div class="other-themes-wrap">
+ <label for="other-themes"><?php esc_html_e( 'Select another theme to view its custom CSS.', 'jetpack' ); ?></label>
+ <select id="other-themes">
+ <option value=""><?php esc_html_e( 'Select a theme&hellip;', 'jetpack' ); ?></option>
+ <?php
+ foreach ( $themes as $theme_stylesheet => $data ) {
+ $revisions = wp_get_post_revisions( $data['post']->ID, array( 'posts_per_page' => 1 ) );
+ if ( ! $revisions ) {
+ ?>
+ <option value="<?php echo esc_url( add_query_arg( 'id', $data['post']->ID, menu_page_url( 'editcss', 0 ) ) ); ?>" <?php disabled( $stylesheet, $theme_stylesheet ); ?>>
+ <?php echo esc_html( $data['label'] ); ?>
+ <?php printf( esc_html__( '(modified %s ago)', 'jetpack' ), human_time_diff( strtotime( $data['post']->post_modified_gmt ) ) ); ?></option>
+ <?php
+ continue;
+ }
+ $revision = array_shift( $revisions );
+ ?>
+ <option value="<?php echo esc_url( get_edit_post_link( $revision->ID ) ); ?>" <?php disabled( $stylesheet, $theme_stylesheet ); ?>>
+ <?php echo esc_html( $data['label'] ); ?>
+ <?php printf( esc_html__( '(modified %s ago)', 'jetpack' ), human_time_diff( strtotime( $data['post']->post_modified_gmt ) ) ); ?></option>
+ <?php
+ }
+ ?>
+ </select>
+ <button class="button" id="other_theme_custom_css_switcher"><?php esc_html_e( 'Switch', 'jetpack' ); ?></button>
+ </div>
+ <?php
+ }
+}
+
+Jetpack_Custom_CSS_Enhancements::add_hooks();
+
+if ( ! function_exists( 'safecss_class' ) ) :
+ /**
+ * Load in the class only when needed. Makes lighter load by having one less class in memory.
+ */
+ function safecss_class() {
+ // Wrapped so we don't need the parent class just to load the plugin.
+ if ( class_exists( 'safecss' ) ) {
+ return;
+ }
+
+ require_once( dirname( __FILE__ ) . '/csstidy/class.csstidy.php' );
+
+ /**
+ * Class safecss
+ */
+ class safecss extends csstidy_optimise {
+
+ /**
+ * Optimises $css after parsing.
+ */
+ function postparse() {
+
+ /** This action is documented in modules/custom-css/custom-css.php */
+ do_action( 'csstidy_optimize_postparse', $this );
+
+ return parent::postparse();
+ }
+
+ /**
+ * Optimises a sub-value.
+ */
+ function subvalue() {
+
+ /** This action is documented in modules/custom-css/custom-css.php */
+ do_action( 'csstidy_optimize_subvalue', $this );
+
+ return parent::subvalue();
+ }
+ }
+ }
+endif;
diff --git a/plugins/jetpack/modules/custom-css/custom-css.php b/plugins/jetpack/modules/custom-css/custom-css.php
new file mode 100644
index 00000000..6229014b
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css.php
@@ -0,0 +1,1866 @@
+<?php
+
+class Jetpack_Custom_CSS {
+ static function init() {
+ add_action( 'switch_theme', array( __CLASS__, 'reset' ) );
+ add_action( 'wp_restore_post_revision', array( __CLASS__, 'restore_revision' ), 10, 2 );
+
+ // Save revisions for posts of type safecss.
+ add_action( 'load-revision.php', array( __CLASS__, 'add_revision_redirect' ) );
+
+ // Override the edit link, the default link causes a redirect loop
+ add_filter( 'get_edit_post_link', array( __CLASS__, 'revision_post_link' ), 10, 3 );
+
+ // Overwrite the content width global variable if one is set in the custom css
+ add_action( 'template_redirect', array( __CLASS__, 'set_content_width' ) );
+ add_action( 'admin_init', array( __CLASS__, 'set_content_width' ) );
+
+ if ( ! is_admin() )
+ add_filter( 'stylesheet_uri', array( __CLASS__, 'style_filter' ) );
+
+ define(
+ 'SAFECSS_USE_ACE',
+ ! jetpack_is_mobile() &&
+ ! Jetpack_User_Agent_Info::is_ipad() &&
+ /**
+ * Should the Custom CSS module use ACE to process CSS.
+ * @see http://ace.c9.io/
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param bool true Use ACE to process the Custom CSS. Default to true.
+ */
+ apply_filters( 'safecss_use_ace', true )
+ );
+
+ // Register safecss as a custom post_type
+ // Explicit capability definitions are largely unnecessary because the posts are manipulated in code via an options page, managing CSS revisions does check the capabilities, so let's ensure that the proper caps are checked.
+ register_post_type( 'safecss', array(
+ // These are the defaults
+ // 'exclude_from_search' => true,
+ // 'public' => false,
+ // 'publicly_queryable' => false,
+ // 'show_ui' => false,
+ 'supports' => array( 'revisions' ),
+ 'label' => 'Custom CSS',
+ 'can_export' => false,
+ 'rewrite' => false,
+ 'capabilities' => array(
+ 'edit_post' => 'edit_theme_options',
+ 'read_post' => 'read',
+ 'delete_post' => 'edit_theme_options',
+ 'edit_posts' => 'edit_theme_options',
+ 'edit_others_posts' => 'edit_theme_options',
+ 'publish_posts' => 'edit_theme_options',
+ 'read_private_posts' => 'read'
+ )
+ ) );
+
+ // Short-circuit WP if this is a CSS stylesheet request
+ if ( isset( $_GET['custom-css'] ) ) {
+ header( 'Content-Type: text/css', true, 200 );
+ header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + 31536000) . ' GMT' ); // 1 year
+ Jetpack_Custom_CSS::print_css();
+ exit;
+ }
+
+ add_action( 'admin_enqueue_scripts', array( 'Jetpack_Custom_CSS', 'enqueue_scripts' ) );
+
+ if ( isset( $_GET['page'] ) && 'editcss' == $_GET['page'] && is_admin() ) {
+ // Do migration routine if necessary
+ Jetpack_Custom_CSS::upgrade();
+
+ /**
+ * Allows additional work when migrating safecss from wp_options to wp_post.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ */
+ do_action( 'safecss_migrate_post' );
+ }
+
+ /**
+ * Never embed the style in the head on wpcom.
+ * Yes, this filter should be added to an unsynced file on wpcom, but
+ * there is no good syntactically-correct location to put it yet.
+ * @link https://github.com/Automattic/jetpack/commit/a1be114e9179f64d147124727a58e2cf76c7e5a1#commitcomment-7763921
+ */
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ add_filter( 'safecss_embed_style', '__return_false' );
+ } else {
+ add_filter( 'safecss_embed_style', array( 'Jetpack_Custom_CSS', 'should_we_inline_custom_css' ), 10, 2 );
+ }
+
+ add_action( 'wp_head', array( 'Jetpack_Custom_CSS', 'link_tag' ), 101 );
+
+ add_filter( 'jetpack_content_width', array( 'Jetpack_Custom_CSS', 'jetpack_content_width' ) );
+ add_filter( 'editor_max_image_size', array( 'Jetpack_Custom_CSS', 'editor_max_image_size' ), 10, 3 );
+
+ if ( !current_user_can( 'switch_themes' ) && !is_super_admin() )
+ return;
+
+ add_action( 'admin_menu', array( 'Jetpack_Custom_CSS', 'menu' ) );
+
+ if ( isset( $_POST['safecss'] ) && false == strstr( $_SERVER[ 'REQUEST_URI' ], 'options.php' ) ) {
+ check_admin_referer( 'safecss' );
+
+ $save_result = self::save( array(
+ 'css' => stripslashes( $_POST['safecss'] ),
+ 'is_preview' => isset( $_POST['action'] ) && $_POST['action'] == 'preview',
+ 'preprocessor' => isset( $_POST['custom_css_preprocessor'] ) ? $_POST['custom_css_preprocessor'] : '',
+ 'add_to_existing' => isset( $_POST['add_to_existing'] ) ? $_POST['add_to_existing'] == 'true' : true,
+ 'content_width' => isset( $_POST['custom_content_width'] ) ? $_POST['custom_content_width'] : false,
+ ) );
+
+ if ( $_POST['action'] == 'preview' ) {
+ wp_safe_redirect( add_query_arg( 'csspreview', 'true', get_option( 'home' ) ) );
+ exit;
+ }
+
+ if ( $save_result )
+ add_action( 'admin_notices', array( 'Jetpack_Custom_CSS', 'saved_message' ) );
+ }
+
+ // Prevent content filters running on CSS when restoring revisions
+ if ( isset( $_REQUEST[ 'action' ] ) && 'restore' === $_REQUEST[ 'action' ] && false !== strstr( $_SERVER[ 'REQUEST_URI' ], 'revision.php' ) ) {
+ $parent_post = get_post( wp_get_post_parent_id( intval( $_REQUEST[ 'revision' ] ) ) );
+ if ( $parent_post && ! is_wp_error( $parent_post ) && 'safecss' === $parent_post->post_type ) {
+ // Remove wp_filter_post_kses, this causes CSS escaping issues
+ remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
+ remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' );
+ remove_all_filters( 'content_save_pre' );
+ }
+ }
+
+ // Modify all internal links so that preview state persists
+ if ( Jetpack_Custom_CSS::is_preview() )
+ ob_start( array( 'Jetpack_Custom_CSS', 'buffer' ) );
+ }
+
+ /**
+ * Save new custom CSS. This should be the entry point for any third-party code using Jetpack_Custom_CSS
+ * to save CSS.
+ *
+ * @param array $args Array of arguments:
+ * string $css The CSS (or LESS or Sass)
+ * bool $is_preview Whether this CSS is preview or published
+ * string preprocessor Which CSS preprocessor to use
+ * bool $add_to_existing Whether this CSS replaces the theme's CSS or supplements it.
+ * int $content_width A custom $content_width to go along with this CSS.
+ * @return int The post ID of the saved Custom CSS post.
+ */
+ public static function save( $args = array() ) {
+ $defaults = array(
+ 'css' => '',
+ 'is_preview' => false,
+ 'preprocessor' => '',
+ 'add_to_existing' => true,
+ 'content_width' => false,
+ );
+
+ $args = wp_parse_args( $args, $defaults );
+
+ if ( $args['content_width'] && intval( $args['content_width']) > 0 && ( ! isset( $GLOBALS['content_width'] ) || $args['content_width'] != $GLOBALS['content_width'] ) )
+ $args['content_width'] = intval( $args['content_width'] );
+ else
+ $args['content_width'] = false;
+
+ // Remove wp_filter_post_kses, this causes CSS escaping issues
+ remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
+ remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' );
+ remove_all_filters( 'content_save_pre' );
+
+ /**
+ * Fires prior to saving custom css values. Necessitated because the
+ * core WordPress save_pre filters were removed:
+ * - content_save_pre
+ * - content_filtered_save_pre
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param array $args {
+ * Array of custom CSS arguments.
+ * @type string $css The CSS (or LESS or Sass).
+ * @type bool $is_preview Whether this CSS is preview or published.
+ * @type string preprocessor Which CSS preprocessor to use.
+ * @type bool $add_to_existing Whether this CSS replaces the theme's CSS or supplements it.
+ * @type int $content_width A custom $content_width to go along with this CSS.
+ * }
+ */
+ do_action( 'safecss_save_pre', $args );
+
+ $warnings = array();
+
+ safecss_class();
+ $csstidy = new csstidy();
+ $csstidy->optimise = new safecss( $csstidy );
+
+ $csstidy->set_cfg( 'remove_bslash', false );
+ $csstidy->set_cfg( 'compress_colors', false );
+ $csstidy->set_cfg( 'compress_font-weight', false );
+ $csstidy->set_cfg( 'optimise_shorthands', 0 );
+ $csstidy->set_cfg( 'remove_last_;', false );
+ $csstidy->set_cfg( 'case_properties', false );
+ $csstidy->set_cfg( 'discard_invalid_properties', true );
+ $csstidy->set_cfg( 'css_level', 'CSS3.0' );
+ $csstidy->set_cfg( 'preserve_css', true );
+ $csstidy->set_cfg( 'template', dirname( __FILE__ ) . '/csstidy/wordpress-standard.tpl' );
+
+ $css = $orig = $args['css'];
+
+ $css = preg_replace( '/\\\\([0-9a-fA-F]{4})/', '\\\\\\\\$1', $prev = $css );
+ // prevent content: '\3434' from turning into '\\3434'
+ $css = str_replace( array( '\'\\\\', '"\\\\' ), array( '\'\\', '"\\' ), $css );
+
+ if ( $css != $prev )
+ $warnings[] = 'preg_replace found stuff';
+
+ // Some people put weird stuff in their CSS, KSES tends to be greedy
+ $css = str_replace( '<=', '&lt;=', $css );
+ // Why KSES instead of strip_tags? Who knows?
+ $css = wp_kses_split( $prev = $css, array(), array() );
+ $css = str_replace( '&gt;', '>', $css ); // kses replaces lone '>' with &gt;
+ // Why both KSES and strip_tags? Because we just added some '>'.
+ $css = strip_tags( $css );
+
+ if ( $css != $prev )
+ $warnings[] = 'kses found stuff';
+
+ // if we're not using a preprocessor
+ if ( ! $args['preprocessor'] ) {
+
+ /**
+ * Fires before parsing the css with CSSTidy, but only if
+ * the preprocessor is not configured for use.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param obj $csstidy The csstidy object.
+ * @param string $css Custom CSS.
+ * @param array $args Array of custom CSS arguments.
+ */
+ do_action( 'safecss_parse_pre', $csstidy, $css, $args );
+
+ $csstidy->parse( $css );
+
+ /**
+ * Fires after parsing the css with CSSTidy, but only if
+ * the preprocessor is not cinfigured for use.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param obj $csstidy The csstidy object.
+ * @param array $warnings Array of warnings.
+ * @param array $args Array of custom CSS arguments.
+ */
+ do_action( 'safecss_parse_post', $csstidy, $warnings, $args );
+
+ $css = $csstidy->print->plain();
+ }
+
+ if ( $args['add_to_existing'] )
+ $add_to_existing = 'yes';
+ else
+ $add_to_existing = 'no';
+
+ if ( $args['is_preview'] || Jetpack_Custom_CSS::is_freetrial() ) {
+ // Save the CSS
+ $safecss_revision_id = Jetpack_Custom_CSS::save_revision( $css, true, $args['preprocessor'] );
+
+ // Cache Buster
+ update_option( 'safecss_preview_rev', intval( get_option( 'safecss_preview_rev' ) ) + 1);
+
+ update_metadata( 'post', $safecss_revision_id, 'custom_css_add', $add_to_existing );
+ update_metadata( 'post', $safecss_revision_id, 'content_width', $args['content_width'] );
+ update_metadata( 'post', $safecss_revision_id, 'custom_css_preprocessor', $args['preprocessor'] );
+
+ delete_option( 'safecss_add' );
+ delete_option( 'safecss_content_width' );
+
+ if ( $args['is_preview'] ) {
+ return $safecss_revision_id;
+ }
+
+ /**
+ * Fires after saving Custom CSS.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ */
+ do_action( 'safecss_save_preview_post' );
+ }
+
+ // Save the CSS
+ $safecss_post_id = Jetpack_Custom_CSS::save_revision( $css, false, $args['preprocessor'] );
+
+ $safecss_post_revision = Jetpack_Custom_CSS::get_current_revision();
+
+ update_option( 'safecss_rev', intval( get_option( 'safecss_rev' ) ) + 1 );
+
+ update_post_meta( $safecss_post_id, 'custom_css_add', $add_to_existing );
+ update_post_meta( $safecss_post_id, 'content_width', $args['content_width'] );
+ update_post_meta( $safecss_post_id, 'custom_css_preprocessor', $args['preprocessor'] );
+
+ delete_option( 'safecss_add' );
+ delete_option( 'safecss_content_width' );
+
+ update_metadata( 'post', $safecss_post_revision['ID'], 'custom_css_add', $add_to_existing );
+ update_metadata( 'post', $safecss_post_revision['ID'], 'content_width', $args['content_width'] );
+ update_metadata( 'post', $safecss_post_revision['ID'], 'custom_css_preprocessor', $args['preprocessor'] );
+
+ delete_option( 'safecss_preview_add' );
+
+ return $safecss_post_id;
+ }
+
+ /**
+ * Get the published custom CSS post.
+ *
+ * @return array
+ */
+ static function get_post() {
+ $custom_css_post_id = Jetpack_Custom_CSS::post_id();
+
+ if ( $custom_css_post_id )
+ return get_post( $custom_css_post_id, ARRAY_A );
+
+ return array();
+ }
+
+ /**
+ * Get the post ID of the published custom CSS post.
+ *
+ * @return int|bool The post ID if it exists; false otherwise.
+ */
+ static function post_id() {
+ /**
+ * Filter the ID of the post where Custom CSS is stored, before the ID is retrieved.
+ *
+ * If the callback function returns a non-null value, then post_id() will immediately
+ * return that value, instead of retrieving the normal post ID.
+ *
+ * @module custom-css
+ *
+ * @since 3.8.1
+ *
+ * @param null null The ID to return instead of the normal ID.
+ */
+ $custom_css_post_id = apply_filters( 'jetpack_custom_css_pre_post_id', null );
+ if ( ! is_null( $custom_css_post_id ) ) {
+ return $custom_css_post_id;
+ }
+
+ $custom_css_post_id = wp_cache_get( 'custom_css_post_id' );
+
+ if ( false === $custom_css_post_id ) {
+ $custom_css_posts = get_posts( array(
+ 'posts_per_page' => 1,
+ 'post_type' => 'safecss',
+ 'post_status' => 'publish',
+ 'orderby' => 'date',
+ 'order' => 'DESC'
+ ) );
+
+ if ( count( $custom_css_posts ) > 0 )
+ $custom_css_post_id = $custom_css_posts[0]->ID;
+ else
+ $custom_css_post_id = 0;
+
+ // Save post_id=0 to note that no safecss post exists.
+ wp_cache_set( 'custom_css_post_id', $custom_css_post_id );
+ }
+
+ if ( ! $custom_css_post_id )
+ return false;
+
+ return $custom_css_post_id;
+ }
+
+ /**
+ * Get the current revision of the original safecss record
+ *
+ * @return object
+ */
+ static function get_current_revision() {
+ $safecss_post = Jetpack_Custom_CSS::get_post();
+
+ if ( empty( $safecss_post ) ) {
+ return false;
+ }
+
+ $revisions = wp_get_post_revisions( $safecss_post['ID'], array( 'posts_per_page' => 1, 'orderby' => 'date', 'order' => 'DESC' ) );
+
+ // Empty array if no revisions exist
+ if ( empty( $revisions ) ) {
+ // Return original post
+ return $safecss_post;
+ } else {
+ // Return the first entry in $revisions, this will be the current revision
+ $current_revision = get_object_vars( array_shift( $revisions ) );
+ return $current_revision;
+ }
+ }
+
+ /**
+ * Save new revision of CSS
+ * Checks to see if content was modified before really saving
+ *
+ * @param string $css
+ * @param bool $is_preview
+ * @return bool|int If nothing was saved, returns false. If a post
+ * or revision was saved, returns the post ID.
+ */
+ static function save_revision( $css, $is_preview = false, $preprocessor = '' ) {
+ $safecss_post = Jetpack_Custom_CSS::get_post();
+
+ $compressed_css = Jetpack_Custom_CSS::minify( $css, $preprocessor );
+
+ // If null, there was no original safecss record, so create one
+ if ( null == $safecss_post ) {
+ if ( ! $css )
+ return false;
+
+ $post = array();
+ $post['post_content'] = wp_slash( $css );
+ $post['post_title'] = 'safecss';
+ $post['post_status'] = 'publish';
+ $post['post_type'] = 'safecss';
+ $post['post_content_filtered'] = wp_slash( $compressed_css );
+
+ // Set excerpt to current theme, for display in revisions list
+ $current_theme = wp_get_theme();
+ $post['post_excerpt'] = $current_theme->Name;
+
+ // Insert the CSS into wp_posts
+ $post_id = wp_insert_post( $post );
+ wp_cache_set( 'custom_css_post_id', $post_id );
+ return $post_id;
+ }
+
+ // Update CSS in post array with new value passed to this function
+ $safecss_post['post_content'] = $css;
+ $safecss_post['post_content_filtered'] = $compressed_css;
+
+ // Set excerpt to current theme, for display in revisions list
+ $current_theme = wp_get_theme();
+ $safecss_post['post_excerpt'] = $current_theme->Name;
+
+ // Don't carry over last revision's timestamps, otherwise revisions all have matching timestamps
+ unset( $safecss_post['post_date'] );
+ unset( $safecss_post['post_date_gmt'] );
+ unset( $safecss_post['post_modified'] );
+ unset( $safecss_post['post_modified_gmt'] );
+
+ // Do not update post if we are only saving a preview
+ if ( false === $is_preview ) {
+ $safecss_post['post_content'] = wp_slash( $safecss_post['post_content'] );
+ $safecss_post['post_content_filtered'] = wp_slash( $safecss_post['post_content_filtered'] );
+ $post_id = wp_update_post( $safecss_post );
+ wp_cache_set( 'custom_css_post_id', $post_id );
+ return $post_id;
+ }
+ else if ( ! defined( 'DOING_MIGRATE' ) ) {
+ return _wp_put_post_revision( $safecss_post );
+ }
+ }
+
+ static function skip_stylesheet() {
+ /**
+ * Prevent the Custom CSS stylesheet from being enqueued.
+ *
+ * @module custom-css
+ *
+ * @since 2.2.1
+ *
+ * @param null Should the stylesheet be skipped. Default to null. Anything else will force the stylesheet to be skipped.
+ */
+ $skip_stylesheet = apply_filters( 'safecss_skip_stylesheet', null );
+
+ if ( null !== $skip_stylesheet ) {
+ return $skip_stylesheet;
+ } elseif ( Jetpack_Custom_CSS::is_customizer_preview() ) {
+ return false;
+ } else {
+ if ( Jetpack_Custom_CSS::is_preview() ) {
+ $safecss_post = Jetpack_Custom_CSS::get_current_revision();
+
+ if ( $safecss_post )
+ return (bool) ( get_post_meta( $safecss_post['ID'], 'custom_css_add', true ) == 'no' );
+ else
+ return (bool) ( get_option( 'safecss_preview_add' ) == 'no' );
+ }
+ else {
+ $custom_css_post_id = Jetpack_Custom_CSS::post_id();
+
+ if ( $custom_css_post_id ) {
+ $custom_css_add = get_post_meta( $custom_css_post_id, 'custom_css_add', true );
+
+ // It is possible for the CSS to be stored in a post but for the safecss_add option
+ // to have not been upgraded yet if the user hasn't opened their Custom CSS editor
+ // since October 2012.
+ if ( ! empty( $custom_css_add ) )
+ return (bool) ( $custom_css_add === 'no' );
+ }
+
+ return (bool) ( Jetpack_Options::get_option_and_ensure_autoload( 'safecss_add', '' ) == 'no' );
+ }
+ }
+ }
+
+ static function is_preview() {
+ return isset( $_GET['csspreview'] ) && $_GET['csspreview'] === 'true';
+ }
+
+ /**
+ * Currently this filter function gets called on
+ * 'template_redirect' action and
+ * 'admin_init' action
+ */
+ static function set_content_width(){
+ // Don't apply this filter on the Edit CSS page
+ if ( isset( $_GET ) && isset( $_GET['page'] ) && 'editcss' == $_GET['page'] && is_admin() ) {
+ return;
+ }
+
+ $GLOBALS['content_width'] = Jetpack::get_content_width();
+ }
+
+ /*
+ * False when the site has the Custom Design upgrade.
+ * Used only on WordPress.com.
+ */
+ static function is_freetrial() {
+ /**
+ * Determine if a WordPress.com site uses a Free trial of the Custom Design Upgrade.
+ * Used only on WordPress.com.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param bool false Does the site use a Free trial of the Custom Design Upgrade. Default to false.
+ */
+ return apply_filters( 'safecss_is_freetrial', false );
+ }
+
+ static function get_preprocessor_key() {
+ $safecss_post = Jetpack_Custom_CSS::get_current_revision();
+ return get_post_meta( $safecss_post['ID'], 'custom_css_preprocessor', true );
+ }
+
+ static function get_preprocessor() {
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
+ $selected_preprocessor_key = self::get_preprocessor_key();
+ $selected_preprocessor = isset( $preprocessors[ $selected_preprocessor_key ] ) ? $preprocessors[ $selected_preprocessor_key ] : null;
+ return $selected_preprocessor;
+ }
+
+ static function get_css( $compressed = false ) {
+ /**
+ * Filter the Custom CSS returned.
+ * Can be used to return an error, or no CSS at all.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param bool false Should we return an error instead of the Custom CSS. Default to false.
+ */
+ $default_css = apply_filters( 'safecss_get_css_error', false );
+
+ if ( $default_css !== false )
+ return $default_css;
+
+ $option = ( Jetpack_Custom_CSS::is_preview() || Jetpack_Custom_CSS::is_freetrial() ) ? 'safecss_preview' : 'safecss';
+ $css = '';
+
+ if ( 'safecss' == $option ) {
+ // Don't bother checking for a migrated 'safecss' option if it never existed.
+ if ( false === get_option( 'safecss' ) || get_option( 'safecss_revision_migrated' ) ) {
+ $safecss_post = Jetpack_Custom_CSS::get_post();
+ if ( ! empty( $safecss_post ) ) {
+ $css = ( $compressed && $safecss_post['post_content_filtered'] ) ? $safecss_post['post_content_filtered'] : $safecss_post['post_content'];
+ }
+ } else {
+ $current_revision = Jetpack_Custom_CSS::get_current_revision();
+ if ( false === $current_revision ) {
+ $css = '';
+ } else {
+ $css = ( $compressed && $current_revision['post_content_filtered'] ) ? $current_revision['post_content_filtered'] : $current_revision['post_content'];
+ }
+ }
+
+ // Fix for un-migrated Custom CSS
+ if ( empty( $safecss_post ) ) {
+ $_css = get_option( 'safecss' );
+ if ( !empty( $_css ) ) {
+ $css = $_css;
+ }
+ }
+ }
+ else if ( 'safecss_preview' == $option ) {
+ $safecss_post = Jetpack_Custom_CSS::get_current_revision();
+ $css = $safecss_post['post_content'];
+ $css = Jetpack_Custom_CSS::minify( $css, get_post_meta( $safecss_post['ID'], 'custom_css_preprocessor', true ) );
+ }
+
+ $css = str_replace( array( '\\\00BB \\\0020', '\0BB \020', '0BB 020' ), '\00BB \0020', $css );
+
+ if ( empty( $css ) ) {
+ $css = "/*\n"
+ . wordwrap(
+ /**
+ * Filter the default message displayed in the Custom CSS editor.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param string $str Default Custom CSS editor content.
+ */
+ apply_filters(
+ 'safecss_default_css',
+ __(
+ "Welcome to Custom CSS!\n\nTo learn how this works, see http://wp.me/PEmnE-Bt",
+ 'jetpack'
+ )
+ )
+ )
+ . "\n*/";
+ }
+
+ /**
+ * Filter the Custom CSS returned from the editor.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param string $css Custom CSS.
+ */
+ $css = apply_filters( 'safecss_css', $css );
+
+ return $css;
+ }
+
+ static function replace_insecure_urls( $css ) {
+ if ( ! function_exists( '_sa_get_frontend_https_url_replacement_map' ) ) {
+ return $css;
+ }
+ list( $http_urls, $secure_urls ) = _sa_get_frontend_https_url_replacement_map();
+
+ return str_replace( $http_urls, $secure_urls, $css );
+ }
+
+ static function print_css() {
+
+ /**
+ * Fires right before printing the custom CSS inside the <head> element.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ */
+ do_action( 'safecss_print_pre' );
+ $css = Jetpack_Custom_CSS::get_css( true );
+ echo self::replace_insecure_urls( $css );
+ }
+
+ static function should_we_inline_custom_css( $should_we, $css ) {
+ // If the CSS is less than 2,000 characters, inline it! otherwise return what was passed in.
+ return ( strlen( $css ) < 2000 ) ? true : $should_we;
+ }
+
+ static function link_tag() {
+ global $blog_id, $current_blog;
+
+ if (
+ /**
+ * Do not include any CSS on the page if the CSS includes an error.
+ * Setting this filter to true stops any Custom CSS from being enqueued.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param bool false Does the CSS include an error. Default to false.
+ */
+ apply_filters( 'safecss_style_error', false )
+ ) {
+ return;
+ }
+
+ if ( ! is_super_admin() && isset( $current_blog ) && ( 1 == $current_blog->spam || 1 == $current_blog->deleted ) )
+ return;
+
+ if ( Jetpack_Custom_CSS::is_customizer_preview() )
+ return;
+
+ $css = '';
+ $option = Jetpack_Custom_CSS::is_preview() ? 'safecss_preview' : 'safecss';
+
+ if ( 'safecss' == $option ) {
+ if ( Jetpack_Options::get_option_and_ensure_autoload( 'safecss_revision_migrated', '0' ) ) {
+ $safecss_post = Jetpack_Custom_CSS::get_post();
+
+ if ( ! empty( $safecss_post['post_content'] ) ) {
+ $css = $safecss_post['post_content'];
+ }
+ } else {
+ $current_revision = Jetpack_Custom_CSS::get_current_revision();
+
+ if ( ! empty( $current_revision['post_content'] ) ) {
+ $css = $current_revision['post_content'];
+ }
+ }
+
+ // Fix for un-migrated Custom CSS
+ if ( empty( $safecss_post ) ) {
+ $_css = Jetpack_Options::get_option_and_ensure_autoload( 'safecss', '' );
+ if ( !empty( $_css ) ) {
+ $css = $_css;
+ }
+ }
+ }
+
+ if ( 'safecss_preview' == $option ) {
+ $safecss_post = Jetpack_Custom_CSS::get_current_revision();
+
+ if ( !empty( $safecss_post['post_content'] ) ) {
+ $css = $safecss_post['post_content'];
+ }
+ }
+
+ $css = str_replace( array( '\\\00BB \\\0020', '\0BB \020', '0BB 020' ), '\00BB \0020', $css );
+
+ if ( $css == '' )
+ return;
+
+ if (
+ /**
+ * Allow inserting CSS inline instead of through a separate file.
+ *
+ * @module custom-css
+ *
+ * @since 3.4.0
+ *
+ * @param bool false Should the CSS be added inline instead of through a separate file. Default to false.
+ * @param string $css Custom CSS.
+ */
+ apply_filters( 'safecss_embed_style', false, $css )
+ ) {
+
+ echo "\r\n" . '<style id="custom-css-css">' . Jetpack_Custom_CSS::get_css( true ) . "</style>\r\n";
+
+ } else {
+
+ $href = home_url( '/' );
+ $href = add_query_arg( 'custom-css', 1, $href );
+ $href = add_query_arg( 'csblog', $blog_id, $href );
+ $href = add_query_arg( 'cscache', 6, $href );
+ $href = add_query_arg( 'csrev', (int) get_option( $option . '_rev' ), $href );
+
+ /**
+ * Filter the Custom CSS link enqueued in the head.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param string $href Custom CSS link enqueued in the head.
+ * @param string $blog_id Blog ID.
+ */
+ $href = apply_filters( 'safecss_href', $href, $blog_id );
+
+ if ( Jetpack_Custom_CSS::is_preview() )
+ $href = add_query_arg( 'csspreview', 'true', $href );
+
+ ?>
+ <link rel="stylesheet" id="custom-css-css" type="text/css" href="<?php echo esc_url( $href ); ?>" />
+ <?php
+
+ }
+
+ /**
+ * Fires after creating the <link> in the <head> element for the custom css stylesheet.
+ *
+ * @module custom-css
+ *
+ * @since 2.2.2
+ */
+ do_action( 'safecss_link_tag_post' );
+ }
+
+ static function style_filter( $current ) {
+ if ( Jetpack_Custom_CSS::is_freetrial() && ( ! Jetpack_Custom_CSS::is_preview() || ! current_user_can( 'switch_themes' ) ) )
+ return $current;
+ else if ( Jetpack_Custom_CSS::skip_stylesheet() )
+ /**
+ * Filter the default blank Custom CSS URL.
+ *
+ * @module custom-css
+ *
+ * @since 2.2.1
+ *
+ * @param string $url Default blank Custom CSS URL.
+ */
+ return apply_filters( 'safecss_style_filter_url', plugins_url( 'custom-css/css/blank.css', __FILE__ ) );
+
+ return $current;
+ }
+
+ static function buffer( $html ) {
+ $html = str_replace( '</body>', Jetpack_Custom_CSS::preview_flag(), $html );
+ return preg_replace_callback( '!href=([\'"])(.*?)\\1!', array( 'Jetpack_Custom_CSS', 'preview_links' ), $html );
+ }
+
+ static function preview_links( $matches ) {
+ if ( 0 !== strpos( $matches[2], get_option( 'home' ) ) )
+ return $matches[0];
+
+ $link = wp_specialchars_decode( $matches[2] );
+ $link = add_query_arg( 'csspreview', 'true', $link );
+ $link = esc_url( $link );
+ return "href={$matches[1]}$link{$matches[1]}";
+ }
+
+ /**
+ * Places a black bar above every preview page
+ */
+ static function preview_flag() {
+ if ( is_admin() )
+ return;
+
+ $message = esc_html__( 'Preview: changes must be saved or they will be lost', 'jetpack' );
+ /**
+ * Filter the Preview message displayed on the site when previewing custom CSS, before to save it.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param string $message Custom CSS preview message.
+ */
+ $message = apply_filters( 'safecss_preview_message', $message );
+
+ $preview_flag_js = "var flag = document.createElement('div');
+ flag.innerHTML = " . json_encode( $message ) . ";
+ flag.style.background = '#FF6600';
+ flag.style.color = 'white';
+ flag.style.textAlign = 'center';
+ flag.style.fontSize = '15px';
+ flag.style.padding = '2px';
+ flag.style.fontFamily = 'sans-serif';
+ document.body.style.paddingTop = '0px';
+ document.body.insertBefore(flag, document.body.childNodes[0]);
+ ";
+
+ /**
+ * Filter the Custom CSS preview message JS styling.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param string $preview_flag_js Custom CSS preview message JS styling.
+ */
+ $preview_flag_js = apply_filters( 'safecss_preview_flag_js', $preview_flag_js );
+ if ( $preview_flag_js ) {
+ $preview_flag_js = '<script type="text/javascript">
+ // <![CDATA[
+ ' . $preview_flag_js . '
+ // ]]>
+ </script>';
+ }
+
+ return $preview_flag_js;
+ }
+
+ static function menu() {
+ $parent = 'themes.php';
+ $title = __( 'Edit CSS', 'jetpack' );
+ $hook = add_theme_page( $title, $title, 'edit_theme_options', 'editcss', array( 'Jetpack_Custom_CSS', 'admin' ) );
+
+ add_action( "load-revision.php", array( 'Jetpack_Custom_CSS', 'prettify_post_revisions' ) );
+ add_action( "load-$hook", array( 'Jetpack_Custom_CSS', 'update_title' ) );
+ }
+
+ /**
+ * Adds a menu item in the appearance section for this plugin's administration
+ * page. Also adds hooks to enqueue the CSS and JS for the admin page.
+ */
+ static function update_title() {
+ global $title;
+ $title = __( 'CSS', 'jetpack' );
+ }
+
+ static function prettify_post_revisions() {
+ add_filter( 'the_title', array( 'Jetpack_Custom_CSS', 'post_title' ), 10, 2 );
+ }
+
+ static function post_title( $title, $post_id ) {
+ if ( !$post_id = (int) $post_id ) {
+ return $title;
+ }
+
+ if ( !$post = get_post( $post_id ) ) {
+ return $title;
+ }
+
+ if ( 'safecss' != $post->post_type ) {
+ return $title;
+ }
+
+ return __( 'Custom CSS Stylesheet', 'jetpack' );
+ }
+
+ static function enqueue_scripts( $hook ) {
+ if ( 'appearance_page_editcss' != $hook )
+ return;
+
+ wp_enqueue_script( 'postbox' );
+ wp_enqueue_script(
+ 'custom-css-editor',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/custom-css/custom-css/js/css-editor.min.js',
+ 'modules/custom-css/custom-css/js/css-editor.js'
+ ),
+ 'jquery',
+ '20130325',
+ true
+ );
+ wp_enqueue_style( 'custom-css-editor', plugins_url( 'custom-css/css/css-editor.css', __FILE__ ) );
+
+ if ( defined( 'SAFECSS_USE_ACE' ) && SAFECSS_USE_ACE ) {
+ wp_register_style( 'jetpack-css-codemirror', plugins_url( 'custom-css/css/codemirror.css', __FILE__ ), array(), '20120905' );
+ wp_enqueue_style( 'jetpack-css-use-codemirror', plugins_url( 'custom-css/css/use-codemirror.css', __FILE__ ), array( 'jetpack-css-codemirror' ), '20120905' );
+
+ wp_register_script( 'jetpack-css-codemirror', plugins_url( 'custom-css/js/codemirror.min.js', __FILE__ ), array(), '3.16', true );
+ wp_enqueue_script(
+ 'jetpack-css-use-codemirror',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/custom-css/custom-css/js/use-codemirror.min.js',
+ 'modules/custom-css/custom-css/js/use-codemirror.js'
+ ),
+ array( 'jquery', 'underscore', 'jetpack-css-codemirror' ),
+ '20131009',
+ true
+ );
+ }
+ }
+
+ static function saved_message() {
+ echo '<div id="message" class="updated fade"><p><strong>' . __( 'Stylesheet saved.', 'jetpack' ) . '</strong></p></div>';
+ }
+
+ static function admin() {
+ add_meta_box( 'submitdiv', __( 'Publish', 'jetpack' ), array( __CLASS__, 'publish_box' ), 'editcss', 'side' );
+ add_action( 'custom_css_submitbox_misc_actions', array( __CLASS__, 'content_width_settings' ) );
+
+ $safecss_post = Jetpack_Custom_CSS::get_post();
+
+ if ( ! empty( $safecss_post ) && 0 < $safecss_post['ID'] && wp_get_post_revisions( $safecss_post['ID'], array( 'posts_per_page' => 1 ) ) )
+ add_meta_box( 'revisionsdiv', __( 'CSS Revisions', 'jetpack' ), array( __CLASS__, 'revisions_meta_box' ), 'editcss', 'side' );
+ ?>
+ <div class="wrap">
+ <?php
+
+ /**
+ * Fires right before the custom css page begins.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ */
+ do_action( 'custom_design_header' );
+
+ ?>
+ <h1><?php _e( 'CSS Stylesheet Editor', 'jetpack' ); ?></h1>
+ <form id="safecssform" action="" method="post">
+ <?php wp_nonce_field( 'safecss' ) ?>
+ <?php wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); ?>
+ <?php wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?>
+ <input type="hidden" name="action" value="save" />
+ <div id="poststuff">
+ <p class="css-support">
+ <?php
+ /**
+ * Filter the intro text appearing above the Custom CSS Editor.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param string $str Intro text appearing above the Custom CSS editor.
+ */
+ echo apply_filters( 'safecss_intro_text', __( 'New to CSS? Start with a <a href="http://www.htmldog.com/guides/cssbeginner/" rel="noopener noreferrer" target="_blank">beginner tutorial</a>. Questions?
+ Ask in the <a href="https://wordpress.org/support/forum/themes-and-templates" rel="noopener noreferrer" target="_blank">Themes and Templates forum</a>.', 'jetpack' ) );
+ ?></p>
+ <p class="css-support"><?php echo __( 'Note: Custom CSS will be reset when changing themes.', 'jetpack' ); ?></p>
+
+ <div id="post-body" class="metabox-holder columns-2">
+ <div id="post-body-content">
+ <div class="postarea">
+ <textarea id="safecss" name="safecss"<?php if ( SAFECSS_USE_ACE ) echo ' class="hide-if-js"'; ?>><?php echo esc_textarea( Jetpack_Custom_CSS::get_css() ); ?></textarea>
+ <div class="clear"></div>
+ </div>
+ </div>
+ <div id="postbox-container-1" class="postbox-container">
+ <?php do_meta_boxes( 'editcss', 'side', $safecss_post ); ?>
+ </div>
+ </div>
+ <br class="clear" />
+ </div>
+ </form>
+ </div>
+ <?php
+ }
+
+ /**
+ * Content width setting callback
+ */
+ static function content_width_settings() {
+ $safecss_post = Jetpack_Custom_CSS::get_current_revision();
+
+ $custom_content_width = get_post_meta( $safecss_post['ID'], 'content_width', true );
+
+ // If custom content width hasn't been overridden and the theme has a content_width value, use that as a default.
+ if ( $custom_content_width <= 0 && ! empty( $GLOBALS['content_width'] ) )
+ $custom_content_width = $GLOBALS['content_width'];
+
+ if ( ! $custom_content_width || ( isset( $GLOBALS['content_width'] ) && $custom_content_width == $GLOBALS['content_width'] ) )
+ $custom_content_width = '';
+
+ ?>
+ <div class="misc-pub-section">
+ <label><?php esc_html_e( 'Media Width:', 'jetpack' ); ?></label>
+ <span id="content-width-display" data-default-text="<?php esc_attr_e( 'Default', 'jetpack' ); ?>" data-custom-text="<?php esc_attr_e( '%s px', 'jetpack' ); ?>"><?php echo $custom_content_width ? sprintf( esc_html__( '%s px', 'jetpack' ), $custom_content_width ) : esc_html_e( 'Default', 'jetpack' ); ?></span>
+ <a class="edit-content-width hide-if-no-js" href="#content-width"><?php echo esc_html_e( 'Edit', 'jetpack' ); ?></a>
+ <div id="content-width-select" class="hide-if-js">
+ <input type="hidden" name="custom_content_width" id="custom_content_width" value="<?php echo esc_attr( $custom_content_width ); ?>" />
+ <p>
+ <?php
+
+ printf( /* translators: %1$s is replaced with an input field for numbers. */
+ __( 'Limit width to %1$s pixels for full size images. (<a href="%2$s" rel="noopener noreferrer" target="_blank">More info</a>.)', 'jetpack' ),
+ '<input type="text" id="custom_content_width_visible" value="' . esc_attr( $custom_content_width ) . '" size="4" />',
+ /**
+ * Filter the Custom CSS limited width's support doc URL.
+ *
+ * @module custom-css
+ *
+ * @since 2.2.3
+ *
+ * @param string $url Custom CSS limited width's support doc URL.
+ */
+ apply_filters( 'safecss_limit_width_link', 'http://jetpack.com/support/custom-css/#limited-width' )
+ );
+
+ ?>
+ </p>
+ <?php
+
+ if (
+ ! empty( $GLOBALS['content_width'] )
+ && $custom_content_width != $GLOBALS['content_width']
+ ) {
+ $current_theme = wp_get_theme()->Name;
+
+ ?>
+ <p><?php printf( _n( 'The default content width for the %s theme is %d pixel.', 'The default content width for the %s theme is %d pixels.', intval( $GLOBALS['content_width'] ), 'jetpack' ), $current_theme, intval( $GLOBALS['content_width'] ) ); ?></p>
+ <?php
+ }
+
+ ?>
+ <a class="save-content-width hide-if-no-js button" href="#content-width"><?php esc_html_e( 'OK', 'jetpack' ); ?></a>
+ <a class="cancel-content-width hide-if-no-js" href="#content-width"><?php esc_html_e( 'Cancel', 'jetpack' ); ?></a>
+ </div>
+ <script type="text/javascript">
+ jQuery( function ( $ ) {
+ var defaultContentWidth = <?php echo isset( $GLOBALS['content_width'] ) ? json_encode( intval( $GLOBALS['content_width'] ) ) : 0; ?>;
+
+ $( '.edit-content-width' ).bind( 'click', function ( e ) {
+ e.preventDefault();
+
+ $( '#content-width-select' ).slideDown();
+ $( this ).hide();
+ } );
+
+ $( '.cancel-content-width' ).bind( 'click', function ( e ) {
+ e.preventDefault();
+
+ $( '#content-width-select' ).slideUp( function () {
+ $( '.edit-content-width' ).show();
+ $( '#custom_content_width_visible' ).val( $( '#custom_content_width' ).val() );
+ } );
+ } );
+
+ $( '.save-content-width' ).bind( 'click', function ( e ) {
+ e.preventDefault();
+
+ $( '#content-width-select' ).slideUp();
+
+ var newContentWidth = parseInt( $( '#custom_content_width_visible' ).val(), 10 );
+
+ if ( newContentWidth && newContentWidth != defaultContentWidth ) {
+ $( '#content-width-display' ).text(
+ $( '#content-width-display' )
+ .data( 'custom-text' )
+ .replace( '%s', $( '#custom_content_width_visible' ).val() )
+ );
+ }
+ else {
+ $( '#content-width-display' ).text( $( '#content-width-display' ).data( 'default-text' ) );
+ }
+
+ $( '#custom_content_width' ).val( $( '#custom_content_width_visible' ).val() );
+ $( '.edit-content-width' ).show();
+ } );
+ } );
+ </script>
+ </div>
+ <?php
+ }
+
+ static function publish_box() {
+ ?>
+ <div id="minor-publishing">
+ <div id="misc-publishing-actions">
+ <?php
+
+ /**
+ * Filter the array of available Custom CSS preprocessors.
+ *
+ * @module custom-css
+ *
+ * @since 2.0.3
+ *
+ * @param array array() Empty by default.
+ */
+ $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
+
+ if ( ! empty( $preprocessors ) ) {
+ $safecss_post = Jetpack_Custom_CSS::get_current_revision();
+ $selected_preprocessor_key = get_post_meta( $safecss_post['ID'], 'custom_css_preprocessor', true );
+ $selected_preprocessor = isset( $preprocessors[$selected_preprocessor_key] ) ? $preprocessors[$selected_preprocessor_key] : null;
+
+ ?>
+ <div class="misc-pub-section">
+ <label><?php esc_html_e( 'Preprocessor:', 'jetpack' ); ?></label>
+ <span id="preprocessor-display"><?php echo esc_html( $selected_preprocessor ? $selected_preprocessor['name'] : __( 'None', 'jetpack' ) ); ?></span>
+ <a class="edit-preprocessor hide-if-no-js" href="#preprocessor"><?php echo esc_html_e( 'Edit', 'jetpack' ); ?></a>
+ <div id="preprocessor-select" class="hide-if-js">
+ <input type="hidden" name="custom_css_preprocessor" id="custom_css_preprocessor" value="<?php echo esc_attr( $selected_preprocessor_key ); ?>" />
+ <select id="preprocessor_choices">
+ <option value=""><?php esc_html_e( 'None', 'jetpack' ); ?></option>
+ <?php
+
+ foreach ( $preprocessors as $preprocessor_key => $preprocessor ) {
+ ?>
+ <option value="<?php echo esc_attr( $preprocessor_key ); ?>" <?php selected( $selected_preprocessor_key, $preprocessor_key ); ?>><?php echo esc_html( $preprocessor['name'] ); ?></option>
+ <?php
+ }
+
+ ?>
+ </select>
+ <a class="save-preprocessor hide-if-no-js button" href="#preprocessor"><?php esc_html_e( 'OK', 'jetpack' ); ?></a>
+ <a class="cancel-preprocessor hide-if-no-js" href="#preprocessor"><?php esc_html_e( 'Cancel', 'jetpack' ); ?></a>
+ </div>
+ </div>
+ <?php
+ }
+
+ $safecss_post = Jetpack_Custom_CSS::get_current_revision();
+
+ $add_css = ( get_post_meta( $safecss_post['ID'], 'custom_css_add', true ) != 'no' );
+
+ ?>
+ <div class="misc-pub-section">
+ <label><?php esc_html_e( 'Mode:', 'jetpack' ); ?></label>
+ <span id="css-mode-display"><?php echo esc_html( $add_css ? __( 'Add-on', 'jetpack' ) : __( 'Replacement', 'jetpack' ) ); ?></span>
+ <a class="edit-css-mode hide-if-no-js" href="#css-mode"><?php echo esc_html_e( 'Edit', 'jetpack' ); ?></a>
+ <div id="css-mode-select" class="hide-if-js">
+ <input type="hidden" name="add_to_existing" id="add_to_existing" value="<?php echo $add_css ? 'true' : 'false'; ?>" />
+ <p>
+ <label>
+ <input type="radio" name="add_to_existing_display" value="true" <?php checked( $add_css ); ?>/>
+ <?php _e( 'Add-on CSS <b>(Recommended)</b>', 'jetpack' ); ?>
+ </label>
+ <br />
+ <label>
+ <input type="radio" name="add_to_existing_display" value="false" <?php checked( ! $add_css ); ?>/>
+ <?php printf(
+ __( 'Replace <a href="%s">theme\'s CSS</a> <b>(Advanced)</b>', 'jetpack' ),
+ /**
+ * Filter the theme's stylesheet URL.
+ *
+ * @module custom-css
+ *
+ * @since 1.7.0
+ *
+ * @param string $url Active theme's stylesheet URL. Default to get_stylesheet_uri().
+ */
+ apply_filters( 'safecss_theme_stylesheet_url', get_stylesheet_uri() )
+ ); ?>
+ </label>
+ </p>
+ <a class="save-css-mode hide-if-no-js button" href="#css-mode"><?php esc_html_e( 'OK', 'jetpack' ); ?></a>
+ <a class="cancel-css-mode hide-if-no-js" href="#css-mode"><?php esc_html_e( 'Cancel', 'jetpack' ); ?></a>
+ </div>
+ </div>
+ <?php
+
+ /**
+ * Allows addition of elements to the submit box for custom css on the wp-admin side.
+ *
+ * @module custom-css
+ *
+ * @since 2.0.3
+ */
+ do_action( 'custom_css_submitbox_misc_actions' );
+
+ ?>
+ </div>
+ </div>
+ <div id="major-publishing-actions">
+ <input type="button" class="button" id="preview" name="preview" value="<?php esc_attr_e( 'Preview', 'jetpack' ) ?>" />
+ <div id="publishing-action">
+ <input type="submit" class="button-primary" id="save" name="save" value="<?php ( Jetpack_Custom_CSS::is_freetrial() ) ? esc_attr_e( 'Save &amp; Buy Upgrade', 'jetpack' ) : esc_attr_e( 'Save Stylesheet', 'jetpack' ); ?>" />
+ </div>
+ </div>
+ <?php
+ }
+
+ /**
+ * Render metabox listing CSS revisions and the themes that correspond to the revisions.
+ * Called by safecss_admin
+ *
+ * @global $post
+ * @param array $safecss_post
+ * @uses wp_revisions_to_keep
+ * @uses WP_Query
+ * @uses wp_post_revision_title
+ * @uses esc_html
+ * @uses add_query_arg
+ * @uses menu_page_url
+ * @uses wp_reset_query
+ * @return string
+ */
+ static function revisions_meta_box( $safecss_post ) {
+
+ $show_all_revisions = isset( $_GET['show_all_rev'] );
+
+ if ( function_exists( 'wp_revisions_to_keep' ) ) {
+ $max_revisions = wp_revisions_to_keep( (object) $safecss_post );
+ } else {
+ $max_revisions = defined( 'WP_POST_REVISIONS' ) && is_numeric( WP_POST_REVISIONS ) ? (int) WP_POST_REVISIONS : 25;
+ }
+
+ $posts_per_page = $show_all_revisions ? $max_revisions : 6;
+
+ $revisions = new WP_Query( array(
+ 'posts_per_page' => $posts_per_page,
+ 'post_type' => 'revision',
+ 'post_status' => 'inherit',
+ 'post_parent' => $safecss_post['ID'],
+ 'orderby' => 'date',
+ 'order' => 'DESC'
+ ) );
+
+ if ( $revisions->have_posts() ) { ?>
+ <ul class="post-revisions"><?php
+
+ global $post;
+
+ while ( $revisions->have_posts() ) :
+ $revisions->the_post();
+
+ ?><li>
+ <?php
+ echo wp_post_revision_title( $post );
+
+ if ( ! empty( $post->post_excerpt ) )
+ echo ' (' . esc_html( $post->post_excerpt ) . ')';
+ ?>
+ </li><?php
+
+ endwhile;
+
+ ?></ul><?php
+
+ if ( $revisions->found_posts > 6 && !$show_all_revisions ) {
+ ?>
+ <br>
+ <a href="<?php echo add_query_arg( 'show_all_rev', 'true', menu_page_url( 'editcss', false ) ); ?>"><?php esc_html_e( 'Show all', 'jetpack' ); ?></a>
+ <?php
+ }
+ }
+
+ wp_reset_query();
+ }
+
+ /**
+ * Hook in init at priority 11 to disable custom CSS.
+ */
+ static function disable() {
+ remove_action( 'wp_head', array( 'Jetpack_Custom_CSS', 'link_tag' ), 101 );
+ remove_filter( 'stylesheet_uri', array( 'Jetpack_Custom_CSS', 'style_filter' ) );
+ }
+
+ /**
+ * Reset all aspects of Custom CSS on a theme switch so that changing
+ * themes is a sure-fire way to get a clean start.
+ */
+ static function reset() {
+ $safecss_post_id = Jetpack_Custom_CSS::save_revision( '' );
+ $safecss_revision = Jetpack_Custom_CSS::get_current_revision();
+
+ update_option( 'safecss_rev', intval( get_option( 'safecss_rev' ) ) + 1 );
+
+ update_post_meta( $safecss_post_id, 'custom_css_add', 'yes' );
+ update_post_meta( $safecss_post_id, 'content_width', false );
+ update_post_meta( $safecss_post_id, 'custom_css_preprocessor', '' );
+
+ delete_option( 'safecss_add' );
+ delete_option( 'safecss_content_width' );
+
+ update_metadata( 'post', $safecss_revision['ID'], 'custom_css_add', 'yes' );
+ update_metadata( 'post', $safecss_revision['ID'], 'content_width', false );
+ update_metadata( 'post', $safecss_revision['ID'], 'custom_css_preprocessor', '' );
+
+ delete_option( 'safecss_preview_add' );
+ }
+
+ static function is_customizer_preview() {
+ if ( isset ( $GLOBALS['wp_customize'] ) )
+ return ! $GLOBALS['wp_customize']->is_theme_active();
+
+ return false;
+ }
+
+ static function minify( $css, $preprocessor = '' ) {
+ if ( ! $css )
+ return '';
+
+ if ( $preprocessor ) {
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
+
+ if ( isset( $preprocessors[$preprocessor] ) ) {
+ $css = call_user_func( $preprocessors[$preprocessor]['callback'], $css );
+ }
+ }
+
+ safecss_class();
+ $csstidy = new csstidy();
+ $csstidy->optimise = new safecss( $csstidy );
+
+ $csstidy->set_cfg( 'remove_bslash', false );
+ $csstidy->set_cfg( 'compress_colors', true );
+ $csstidy->set_cfg( 'compress_font-weight', true );
+ $csstidy->set_cfg( 'remove_last_;', true );
+ $csstidy->set_cfg( 'case_properties', true );
+ $csstidy->set_cfg( 'discard_invalid_properties', true );
+ $csstidy->set_cfg( 'css_level', 'CSS3.0' );
+ $csstidy->set_cfg( 'template', 'highest');
+ $csstidy->parse( $css );
+
+ return $csstidy->print->plain();
+ }
+
+ /**
+ * When restoring a SafeCSS post revision, also copy over the
+ * content_width and custom_css_add post metadata.
+ */
+ static function restore_revision( $_post_id, $_revision_id ) {
+ $_post = get_post( $_post_id );
+
+ if ( 'safecss' != $_post->post_type )
+ return;
+
+ $safecss_revision = Jetpack_Custom_CSS::get_current_revision();
+
+ $content_width = get_post_meta( $_revision_id, 'content_width', true );
+ $custom_css_add = get_post_meta( $_revision_id, 'custom_css_add', true );
+ $preprocessor = get_post_meta( $_revision_id, 'custom_css_preprocessor', true );
+
+ update_metadata( 'post', $safecss_revision['ID'], 'content_width', $content_width );
+ update_metadata( 'post', $safecss_revision['ID'], 'custom_css_add', $custom_css_add );
+ update_metadata( 'post', $safecss_revision['ID'], 'custom_css_preprocessor', $preprocessor );
+
+ delete_option( 'safecss_add' );
+ delete_option( 'safecss_content_width' );
+
+ update_post_meta( $_post->ID, 'content_width', $content_width );
+ update_post_meta( $_post->ID, 'custom_css_add', $custom_css_add );
+ update_post_meta( $_post->ID, 'custom_css_preprocessor', $preprocessor );
+
+ delete_option( 'safecss_preview_add' );
+ }
+
+ /**
+ * Migration routine for moving safecss from wp_options to wp_posts to support revisions
+ *
+ * @return void
+ */
+ static function upgrade() {
+ $css = get_option( 'safecss' );
+
+ if ( get_option( 'safecss_revision_migrated' ) ) {
+ return false;
+ }
+
+ // Check if CSS is stored in wp_options
+ if ( $css ) {
+ // Remove the async actions from publish_post
+ remove_action( 'publish_post', 'queue_publish_post' );
+
+ $post = array();
+ $post['post_content'] = $css;
+ $post['post_title'] = 'safecss';
+ $post['post_status'] = 'publish';
+ $post['post_type'] = 'safecss';
+
+ // Insert the CSS into wp_posts
+ $post_id = wp_insert_post( $post );
+ // Check for errors
+ if ( !$post_id or is_wp_error( $post_id ) )
+ die( $post_id->get_error_message() );
+
+ // Delete safecss option
+ delete_option( 'safecss' );
+ }
+
+ unset( $css );
+
+ // Check if we have already done this
+ if ( !get_option( 'safecss_revision_migrated' ) ) {
+ define( 'DOING_MIGRATE', true );
+
+ // Get hashes of safecss post and current revision
+ $safecss_post = Jetpack_Custom_CSS::get_post();
+
+ if ( empty( $safecss_post ) )
+ return;
+
+ $safecss_post_hash = md5( $safecss_post['post_content'] );
+ $current_revision = Jetpack_Custom_CSS::get_current_revision();
+
+ if ( null == $current_revision )
+ return;
+
+ $current_revision_hash = md5( $current_revision['post_content'] );
+
+ // If hashes are not equal, set safecss post with content from current revision
+ if ( $safecss_post_hash !== $current_revision_hash ) {
+ Jetpack_Custom_CSS::save_revision( $current_revision['post_content'] );
+ // Reset post_content to display the migrated revsion
+ $safecss_post['post_content'] = $current_revision['post_content'];
+ }
+
+ // Set option so that we dont keep doing this
+ update_option( 'safecss_revision_migrated', time() );
+ }
+
+ $newest_safecss_post = Jetpack_Custom_CSS::get_current_revision();
+
+ if ( $newest_safecss_post ) {
+ if ( get_option( 'safecss_content_width' ) ) {
+ // Add the meta to the post and the latest revision.
+ update_post_meta( $newest_safecss_post['ID'], 'content_width', get_option( 'safecss_content_width' ) );
+ update_metadata( 'post', $newest_safecss_post['ID'], 'content_width', get_option( 'safecss_content_width' ) );
+
+ delete_option( 'safecss_content_width' );
+ }
+
+ if ( get_option( 'safecss_add' ) ) {
+ update_post_meta( $newest_safecss_post['ID'], 'custom_css_add', get_option( 'safecss_add' ) );
+ update_metadata( 'post', $newest_safecss_post['ID'], 'custom_css_add', get_option( 'safecss_add' ) );
+
+ delete_option( 'safecss_add' );
+ }
+ }
+ }
+
+ /**
+ * Adds a filter to the redirect location in `wp-admin/revisions.php`.
+ */
+ static function add_revision_redirect() {
+ add_filter( 'wp_redirect', array( __CLASS__, 'revision_redirect' ) );
+ }
+
+ /**
+ * Filters the redirect location in `wp-admin/revisions.php`.
+ *
+ * @param string $location The path to redirect to.
+ * @return string
+ */
+ static function revision_redirect( $location ) {
+ $post = get_post();
+
+ if ( ! empty( $post->post_type ) && 'safecss' == $post->post_type ) {
+ $location = 'themes.php?page=editcss';
+
+ if ( 'edit.php' == $location ) {
+ $location = '';
+ }
+ }
+
+ return $location;
+ }
+
+ static function revision_post_link( $post_link, $post_id, $context ) {
+ if ( !$post_id = (int) $post_id ) {
+ return $post_link;
+ }
+
+ if ( !$post = get_post( $post_id ) ) {
+ return $post_link;
+ }
+
+ if ( 'safecss' != $post->post_type ) {
+ return $post_link;
+ }
+
+ $post_link = admin_url( 'themes.php?page=editcss' );
+
+ if ( 'display' == $context ) {
+ return esc_url( $post_link );
+ }
+
+ return esc_url_raw( $post_link );
+ }
+
+ /**
+ * When on the edit screen, make sure the custom content width
+ * setting is applied to the large image size.
+ */
+ static function editor_max_image_size( $dims, $size = 'medium', $context = null ) {
+ list( $width, $height ) = $dims;
+
+ if ( 'large' == $size && 'edit' == $context )
+ $width = Jetpack::get_content_width();
+
+ return array( $width, $height );
+ }
+
+ /**
+ * Override the content_width with a custom value if one is set.
+ */
+ static function jetpack_content_width( $content_width ) {
+ $custom_content_width = 0;
+
+ if ( Jetpack_Custom_CSS::is_preview() ) {
+ $safecss_post = Jetpack_Custom_CSS::get_current_revision();
+ $custom_content_width = intval( get_post_meta( $safecss_post['ID'], 'content_width', true ) );
+ } else if ( ! Jetpack_Custom_CSS::is_freetrial() ) {
+ $custom_css_post_id = Jetpack_Custom_CSS::post_id();
+ if ( $custom_css_post_id )
+ $custom_content_width = intval( get_post_meta( $custom_css_post_id, 'content_width', true ) );
+ }
+
+ if ( $custom_content_width > 0 )
+ $content_width = $custom_content_width;
+
+ return $content_width;
+ }
+}
+
+class Jetpack_Safe_CSS {
+ static function filter_attr( $css, $element = 'div' ) {
+ safecss_class();
+
+ $css = $element . ' {' . $css . '}';
+
+ $csstidy = new csstidy();
+ $csstidy->optimise = new safecss( $csstidy );
+ $csstidy->set_cfg( 'remove_bslash', false );
+ $csstidy->set_cfg( 'compress_colors', false );
+ $csstidy->set_cfg( 'compress_font-weight', false );
+ $csstidy->set_cfg( 'discard_invalid_properties', true );
+ $csstidy->set_cfg( 'merge_selectors', false );
+ $csstidy->set_cfg( 'remove_last_;', false );
+ $csstidy->set_cfg( 'css_level', 'CSS3.0' );
+
+ $css = preg_replace( '/\\\\([0-9a-fA-F]{4})/', '\\\\\\\\$1', $css );
+ $css = wp_kses_split( $css, array(), array() );
+ $csstidy->parse( $css );
+
+ $css = $csstidy->print->plain();
+
+ $css = str_replace( array( "\n","\r","\t" ), '', $css );
+
+ preg_match( "/^{$element}\s*{(.*)}\s*$/", $css, $matches );
+
+ if ( empty( $matches[1] ) )
+ return '';
+
+ return $matches[1];
+ }
+}
+
+function migrate() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::upgrade()' );
+
+ return Jetpack_Custom_CSS::upgrade();
+}
+
+function safecss_revision_redirect( $redirect ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::revision_redirect()' );
+
+ return Jetpack_Custom_CSS::revision_redirect( $redirect );
+}
+
+function safecss_revision_post_link( $post_link, $post_id, $context ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::revision_post_link()' );
+
+ return Jetpack_Custom_CSS::revision_post_link( $post_link, $post_id, $context );
+}
+
+function get_safecss_post() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::get_post()' );
+
+ return Jetpack_Custom_CSS::get_post();
+}
+
+function custom_css_post_id() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::post_id()' );
+
+ return Jetpack_Custom_CSS::post_id();
+}
+
+function get_current_revision() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::get_current_revision()' );
+
+ return Jetpack_Custom_CSS::get_current_revision();
+}
+
+function save_revision( $css, $is_preview = false, $preprocessor = '' ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::save_revision()' );
+
+ return Jetpack_Custom_CSS::save_revision( $css, $is_preview, $preprocessor );
+}
+
+function safecss_skip_stylesheet() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::skip_stylesheet()' );
+
+ return Jetpack_Custom_CSS::skip_stylesheet();
+}
+
+function safecss_init() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::init()' );
+
+ return Jetpack_Custom_CSS::init();
+}
+
+function safecss_is_preview() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::is_preview()' );
+
+ return Jetpack_Custom_CSS::is_preview();
+}
+
+function safecss_is_freetrial() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::is_freetrial()' );
+
+ return Jetpack_Custom_CSS::is_freetrial();
+}
+
+function safecss( $compressed = false ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::get_css()' );
+
+ return Jetpack_Custom_CSS::get_css( $compressed );
+}
+
+function safecss_print() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::print_css()' );
+
+ return Jetpack_Custom_CSS::print_css();
+}
+
+function safecss_style() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::link_tag()' );
+
+ return Jetpack_Custom_CSS::link_tag();
+}
+
+function safecss_style_filter( $current ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::style_filter()' );
+
+ return Jetpack_Custom_CSS::style_filter( $current );
+}
+
+function safecss_buffer( $html ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::buffer()' );
+
+ return Jetpack_Custom_CSS::buffer( $html );
+}
+
+function safecss_preview_links( $matches ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::preview_links()' );
+
+ return Jetpack_Custom_CSS::preview_links( $matches );
+}
+
+function safecss_preview_flag() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::preview_flag()' );
+
+ return Jetpack_Custom_CSS::preview_flag();
+}
+
+function safecss_menu() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::menu()' );
+
+ return Jetpack_Custom_CSS::menu();
+}
+
+function update_title() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::update_title()' );
+
+ return Jetpack_Custom_CSS::update_title();
+}
+
+function safecss_prettify_post_revisions() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::prettify_post_revisions()' );
+
+ return Jetpack_Custom_CSS::prettify_post_revisions();
+}
+
+function safecss_remove_title_excerpt_from_revisions() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::remove_title_excerpt_from_revisions()' );
+
+ return Jetpack_Custom_CSS::remove_title_excerpt_from_revisions();
+}
+
+function safecss_post_title( $title, $post_id ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::post_title()' );
+
+ return Jetpack_Custom_CSS::post_title( $title, $post_id );
+}
+
+function safe_css_enqueue_scripts() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::enqueue_scripts()' );
+
+ return Jetpack_Custom_CSS::enqueue_scripts( null );
+}
+
+function safecss_admin_head() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::admin_head()' );
+
+ return Jetpack_Custom_CSS::admin_head();
+}
+
+function safecss_saved() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::saved_message()' );
+
+ return Jetpack_Custom_CSS::saved_message();
+}
+
+function safecss_admin() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::admin()' );
+
+ return Jetpack_Custom_CSS::admin();
+}
+
+function custom_css_meta_box() {
+ _deprecated_function( __FUNCTION__, '2.1', 'add_meta_box( $id, $title, $callback, \'editcss\', \'side\' )' );
+}
+
+function custom_css_post_revisions_meta_box( $safecss_post ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::revisions_meta_box()' );
+
+ return Jetpack_Custom_CSS::revisions_meta_box( $safecss_post );
+}
+
+function disable_safecss_style() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::disable()' );
+
+ return Jetpack_Custom_CSS::disable();
+}
+
+function custom_css_reset() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::reset()' );
+
+ return Jetpack_Custom_CSS::reset();
+}
+
+function custom_css_is_customizer_preview() {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::is_customizer_preview()' );
+
+ return Jetpack_Custom_CSS::is_customizer_preview();
+}
+
+function custom_css_minify( $css, $preprocessor = '' ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::minify()' );
+
+ return Jetpack_Custom_CSS::minify( $css, $preprocessor );
+}
+
+function custom_css_restore_revision( $_post_id, $_revision_id ) {
+ _deprecated_function( __FUNCTION__, '2.1', 'Jetpack_Custom_CSS::restore_revision()' );
+
+ return Jetpack_Custom_CSS::restore_revision( $_post_id, $_revision_id );
+}
+
+if ( ! function_exists( 'safecss_class' ) ) :
+function safecss_class() {
+ // Wrapped so we don't need the parent class just to load the plugin
+ if ( class_exists('safecss') )
+ return;
+
+ require_once( dirname( __FILE__ ) . '/csstidy/class.csstidy.php' );
+
+ class safecss extends csstidy_optimise {
+
+ function postparse() {
+
+ /**
+ * Fires after parsing the css.
+ *
+ * @module custom-css
+ *
+ * @since 1.8.0
+ *
+ * @param obj $this CSSTidy object.
+ */
+ do_action( 'csstidy_optimize_postparse', $this );
+
+ return parent::postparse();
+ }
+
+ function subvalue() {
+
+ /**
+ * Fires before optimizing the Custom CSS subvalue.
+ *
+ * @module custom-css
+ *
+ * @since 1.8.0
+ *
+ * @param obj $this CSSTidy object.
+ **/
+ do_action( 'csstidy_optimize_subvalue', $this );
+
+ return parent::subvalue();
+ }
+ }
+}
+endif;
+
+if ( ! function_exists( 'safecss_filter_attr' ) ) {
+ function safecss_filter_attr( $css, $element = 'div' ) {
+ return Jetpack_Safe_CSS::filter_attr( $css, $element );
+ }
+}
+
+include_once dirname( __FILE__ ) . '/custom-css/preprocessors.php';
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/blank.css b/plugins/jetpack/modules/custom-css/custom-css/css/blank.css
new file mode 100644
index 00000000..c84ecefc
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/blank.css
@@ -0,0 +1 @@
+/* */ \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/codemirror-rtl.css b/plugins/jetpack/modules/custom-css/custom-css/css/codemirror-rtl.css
new file mode 100644
index 00000000..359717bf
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/codemirror-rtl.css
@@ -0,0 +1,262 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+/* NOAUTORTL */
+.rtl .CodeMirror {
+ direction: rtl; /* code should always be written left to right */
+}
+/* BASICS */
+.CodeMirror {
+ /* Set height, width, borders, and global font properties here */
+ font-family: monospace;
+ height: 400px;
+}
+.CodeMirror-scroll {
+ /* Set scrolling behavior here */
+ overflow: auto;
+}
+
+/* PADDING */
+
+.CodeMirror-lines {
+ padding: 4px 0; /* Vertical padding around content */
+}
+.CodeMirror pre {
+ padding: 0 4px; /* Horizontal padding of content */
+}
+
+.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ background-color: white; /* The little square between H and V scrollbars */
+}
+
+/* GUTTER */
+
+.CodeMirror-gutters {
+ border-left: 1px solid #ddd;
+ background-color: #f7f7f7;
+ white-space: nowrap;
+}
+.CodeMirror-linenumbers {}
+.CodeMirror-linenumber {
+ padding: 0 5px 0 3px;
+ min-width: 20px;
+ text-align: left;
+ color: #999;
+}
+
+/* CURSOR */
+
+.CodeMirror div.CodeMirror-cursor {
+ border-right: 1px solid black;
+ z-index: 3;
+}
+/* Shown when moving in bi-directional text */
+.CodeMirror div.CodeMirror-secondarycursor {
+ border-right: 1px solid silver;
+}
+.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
+ width: auto;
+ border: 0;
+ background: #7e7;
+ z-index: 1;
+}
+/* Can style cursor different in overwrite (non-insert) mode */
+.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {}
+
+.cm-tab { display: inline-block; }
+
+/* DEFAULT THEME */
+
+.cm-s-default .cm-keyword {color: #708;}
+.cm-s-default .cm-atom {color: #219;}
+.cm-s-default .cm-number {color: #164;}
+.cm-s-default .cm-def {color: #00f;}
+.cm-s-default .cm-variable {color: black;}
+.cm-s-default .cm-variable-2 {color: #05a;}
+.cm-s-default .cm-variable-3 {color: #085;}
+.cm-s-default .cm-property {color: black;}
+.cm-s-default .cm-operator {color: black;}
+.cm-s-default .cm-comment {color: #a50;}
+.cm-s-default .cm-string {color: #a11;}
+.cm-s-default .cm-string-2 {color: #f50;}
+.cm-s-default .cm-meta {color: #555;}
+.cm-s-default .cm-error {color: #f00;}
+.cm-s-default .cm-qualifier {color: #555;}
+.cm-s-default .cm-builtin {color: #30a;}
+.cm-s-default .cm-bracket {color: #997;}
+.cm-s-default .cm-tag {color: #170;}
+.cm-s-default .cm-attribute {color: #00c;}
+.cm-s-default .cm-header {color: blue;}
+.cm-s-default .cm-quote {color: #090;}
+.cm-s-default .cm-hr {color: #999;}
+.cm-s-default .cm-link {color: #00c;}
+
+.cm-negative {color: #d44;}
+.cm-positive {color: #292;}
+.cm-header, .cm-strong {font-weight: bold;}
+.cm-em {font-style: italic;}
+.cm-link {text-decoration: underline;}
+
+.cm-invalidchar {color: #f00;}
+
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
+.CodeMirror-activeline-background {background: #e8f2ff;}
+
+/* STOP */
+
+/* The rest of this file contains styles related to the mechanics of
+ the editor. You probably shouldn't touch them. */
+
+.CodeMirror {
+ line-height: 1;
+ position: relative;
+ overflow: hidden;
+ background: white;
+ color: black;
+}
+
+.CodeMirror-scroll {
+ /* 30px is the magic margin used to hide the element's real scrollbars */
+ /* See overflow: hidden in .CodeMirror */
+ margin-bottom: -30px; margin-left: -30px;
+ padding-bottom: 30px; padding-left: 30px;
+ height: 100%;
+ outline: none; /* Prevent dragging from highlighting the element */
+ position: relative;
+}
+.CodeMirror-sizer {
+ position: relative;
+}
+
+/* The fake, visible scrollbars. Used to force redraw during scrolling
+ before actuall scrolling happens, thus preventing shaking and
+ flickering artifacts. */
+.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ position: absolute;
+ z-index: 6;
+ display: none;
+}
+.CodeMirror-vscrollbar {
+ left: 0; top: 0;
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+.CodeMirror-hscrollbar {
+ bottom: 0; right: 0;
+ overflow-y: hidden;
+ overflow-x: scroll;
+}
+.CodeMirror-scrollbar-filler {
+ left: 0; bottom: 0;
+}
+.CodeMirror-gutter-filler {
+ right: 0; bottom: 0;
+}
+
+.CodeMirror-gutters {
+ position: absolute; right: 0; top: 0;
+ padding-bottom: 30px;
+ z-index: 3;
+}
+.CodeMirror-gutter {
+ white-space: normal;
+ height: 100%;
+ padding-bottom: 30px;
+ margin-bottom: -32px;
+ display: inline-block;
+ /* Hack to make IE7 behave */
+ *zoom:1;
+ *display:inline;
+}
+.CodeMirror-gutter-elt {
+ position: absolute;
+ cursor: default;
+ z-index: 4;
+}
+
+.CodeMirror-lines {
+ cursor: text;
+}
+.CodeMirror pre {
+ /* Reset some styles that the rest of the page might have set */ border-radius: 0;
+ border-width: 0;
+ background: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ margin: 0;
+ white-space: pre;
+ word-wrap: normal;
+ line-height: inherit;
+ color: inherit;
+ z-index: 2;
+ position: relative;
+ overflow: visible;
+}
+.CodeMirror-wrap pre {
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ word-break: normal;
+}
+.CodeMirror-code pre {
+ border-left: 30px solid transparent;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+}
+.CodeMirror-wrap .CodeMirror-code pre {
+ border-left: none;
+ width: auto;
+}
+.CodeMirror-linebackground {
+ position: absolute;
+ right: 0; left: 0; top: 0; bottom: 0;
+ z-index: 0;
+}
+
+.CodeMirror-linewidget {
+ position: relative;
+ z-index: 2;
+ overflow: auto;
+}
+
+.CodeMirror-widget {
+}
+
+.CodeMirror-wrap .CodeMirror-scroll {
+ overflow-x: hidden;
+}
+
+.CodeMirror-measure {
+ position: absolute;
+ width: 100%; height: 0px;
+ overflow: hidden;
+ visibility: hidden;
+}
+.CodeMirror-measure pre { position: static; }
+
+.CodeMirror div.CodeMirror-cursor {
+ position: absolute;
+ visibility: hidden;
+ border-left: none;
+ width: 0;
+}
+.CodeMirror-focused div.CodeMirror-cursor {
+ visibility: visible;
+}
+
+.CodeMirror-selected { background: #d9d9d9; }
+.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
+
+.cm-searching {
+ background: #ffa;
+ background: rgba(255, 255, 0, .4);
+}
+
+/* IE7 hack to prevent it from returning funny offsetTops on the spans */
+.CodeMirror span { *vertical-align: text-bottom; }
+
+@media print {
+ /* Hide the cursor when printing */
+ .CodeMirror div.CodeMirror-cursor {
+ visibility: hidden;
+ }
+}
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/codemirror-rtl.min.css b/plugins/jetpack/modules/custom-css/custom-css/css/codemirror-rtl.min.css
new file mode 100644
index 00000000..bb4ede28
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/codemirror-rtl.min.css
@@ -0,0 +1 @@
+.rtl .CodeMirror{direction:rtl}.CodeMirror{font-family:monospace;height:400px}.CodeMirror-scroll{overflow:auto}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-left:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 5px 0 3px;min-width:20px;text-align:left;color:#999}.CodeMirror div.CodeMirror-cursor{border-right:1px solid #000;z-index:3}.CodeMirror div.CodeMirror-secondarycursor{border-right:1px solid silver}.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor{width:auto;border:0;background:#7e7;z-index:1}.cm-tab{display:inline-block}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable{color:#000}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-property{color:#000}.cm-s-default .cm-operator{color:#000}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-error{color:red}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-invalidchar{color:red}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{line-height:1;position:relative;overflow:hidden;background:#fff;color:#000}.CodeMirror-scroll{margin-bottom:-30px;margin-left:-30px;padding-bottom:30px;padding-left:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{left:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;right:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{left:0;bottom:0}.CodeMirror-gutter-filler{right:0;bottom:0}.CodeMirror-gutters{position:absolute;right:0;top:0;padding-bottom:30px;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;padding-bottom:30px;margin-bottom:-32px;display:inline-block}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text}.CodeMirror pre{border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-code pre{border-left:30px solid transparent;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.CodeMirror-wrap .CodeMirror-code pre{border-left:none;width:auto}.CodeMirror-linebackground{position:absolute;right:0;left:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-wrap .CodeMirror-scroll{overflow-x:hidden}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-measure pre{position:static}.CodeMirror div.CodeMirror-cursor{position:absolute;visibility:hidden;border-left:none;width:0}.CodeMirror-focused div.CodeMirror-cursor{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}@media print{.CodeMirror div.CodeMirror-cursor{visibility:hidden}} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/codemirror.css b/plugins/jetpack/modules/custom-css/custom-css/css/codemirror.css
new file mode 100644
index 00000000..db90a5a5
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/codemirror.css
@@ -0,0 +1,262 @@
+/* NOAUTORTL */
+.rtl .CodeMirror {
+ direction: ltr; /* code should always be written left to right */
+}
+/* BASICS */
+.CodeMirror {
+ /* Set height, width, borders, and global font properties here */
+ font-family: monospace;
+ height: 400px;
+}
+.CodeMirror-scroll {
+ /* Set scrolling behavior here */
+ overflow: auto;
+}
+
+/* PADDING */
+
+.CodeMirror-lines {
+ padding: 4px 0; /* Vertical padding around content */
+}
+.CodeMirror pre {
+ padding: 0 4px; /* Horizontal padding of content */
+}
+
+.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ background-color: white; /* The little square between H and V scrollbars */
+}
+
+/* GUTTER */
+
+.CodeMirror-gutters {
+ border-right: 1px solid #ddd;
+ background-color: #f7f7f7;
+ white-space: nowrap;
+}
+.CodeMirror-linenumbers {}
+.CodeMirror-linenumber {
+ padding: 0 3px 0 5px;
+ min-width: 20px;
+ text-align: right;
+ color: #999;
+}
+
+/* CURSOR */
+
+.CodeMirror div.CodeMirror-cursor {
+ border-left: 1px solid black;
+ z-index: 3;
+}
+/* Shown when moving in bi-directional text */
+.CodeMirror div.CodeMirror-secondarycursor {
+ border-left: 1px solid silver;
+}
+.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
+ width: auto;
+ border: 0;
+ background: #7e7;
+ z-index: 1;
+}
+/* Can style cursor different in overwrite (non-insert) mode */
+.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {}
+
+.cm-tab { display: inline-block; }
+
+/* DEFAULT THEME */
+
+.cm-s-default .cm-keyword {color: #708;}
+.cm-s-default .cm-atom {color: #219;}
+.cm-s-default .cm-number {color: #164;}
+.cm-s-default .cm-def {color: #00f;}
+.cm-s-default .cm-variable {color: black;}
+.cm-s-default .cm-variable-2 {color: #05a;}
+.cm-s-default .cm-variable-3 {color: #085;}
+.cm-s-default .cm-property {color: black;}
+.cm-s-default .cm-operator {color: black;}
+.cm-s-default .cm-comment {color: #a50;}
+.cm-s-default .cm-string {color: #a11;}
+.cm-s-default .cm-string-2 {color: #f50;}
+.cm-s-default .cm-meta {color: #555;}
+.cm-s-default .cm-error {color: #f00;}
+.cm-s-default .cm-qualifier {color: #555;}
+.cm-s-default .cm-builtin {color: #30a;}
+.cm-s-default .cm-bracket {color: #997;}
+.cm-s-default .cm-tag {color: #170;}
+.cm-s-default .cm-attribute {color: #00c;}
+.cm-s-default .cm-header {color: blue;}
+.cm-s-default .cm-quote {color: #090;}
+.cm-s-default .cm-hr {color: #999;}
+.cm-s-default .cm-link {color: #00c;}
+
+.cm-negative {color: #d44;}
+.cm-positive {color: #292;}
+.cm-header, .cm-strong {font-weight: bold;}
+.cm-em {font-style: italic;}
+.cm-link {text-decoration: underline;}
+
+.cm-invalidchar {color: #f00;}
+
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
+.CodeMirror-activeline-background {background: #e8f2ff;}
+
+/* STOP */
+
+/* The rest of this file contains styles related to the mechanics of
+ the editor. You probably shouldn't touch them. */
+
+.CodeMirror {
+ line-height: 1;
+ position: relative;
+ overflow: hidden;
+ background: white;
+ color: black;
+}
+
+.CodeMirror-scroll {
+ /* 30px is the magic margin used to hide the element's real scrollbars */
+ /* See overflow: hidden in .CodeMirror */
+ margin-bottom: -30px; margin-right: -30px;
+ padding-bottom: 30px; padding-right: 30px;
+ height: 100%;
+ outline: none; /* Prevent dragging from highlighting the element */
+ position: relative;
+}
+.CodeMirror-sizer {
+ position: relative;
+}
+
+/* The fake, visible scrollbars. Used to force redraw during scrolling
+ before actuall scrolling happens, thus preventing shaking and
+ flickering artifacts. */
+.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ position: absolute;
+ z-index: 6;
+ display: none;
+}
+.CodeMirror-vscrollbar {
+ right: 0; top: 0;
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+.CodeMirror-hscrollbar {
+ bottom: 0; left: 0;
+ overflow-y: hidden;
+ overflow-x: scroll;
+}
+.CodeMirror-scrollbar-filler {
+ right: 0; bottom: 0;
+}
+.CodeMirror-gutter-filler {
+ left: 0; bottom: 0;
+}
+
+.CodeMirror-gutters {
+ position: absolute; left: 0; top: 0;
+ padding-bottom: 30px;
+ z-index: 3;
+}
+.CodeMirror-gutter {
+ white-space: normal;
+ height: 100%;
+ padding-bottom: 30px;
+ margin-bottom: -32px;
+ display: inline-block;
+ /* Hack to make IE7 behave */
+ *zoom:1;
+ *display:inline;
+}
+.CodeMirror-gutter-elt {
+ position: absolute;
+ cursor: default;
+ z-index: 4;
+}
+
+.CodeMirror-lines {
+ cursor: text;
+}
+.CodeMirror pre {
+ /* Reset some styles that the rest of the page might have set */
+ -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
+ border-width: 0;
+ background: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ margin: 0;
+ white-space: pre;
+ word-wrap: normal;
+ line-height: inherit;
+ color: inherit;
+ z-index: 2;
+ position: relative;
+ overflow: visible;
+}
+.CodeMirror-wrap pre {
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ word-break: normal;
+}
+.CodeMirror-code pre {
+ border-right: 30px solid transparent;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+}
+.CodeMirror-wrap .CodeMirror-code pre {
+ border-right: none;
+ width: auto;
+}
+.CodeMirror-linebackground {
+ position: absolute;
+ left: 0; right: 0; top: 0; bottom: 0;
+ z-index: 0;
+}
+
+.CodeMirror-linewidget {
+ position: relative;
+ z-index: 2;
+ overflow: auto;
+}
+
+.CodeMirror-widget {
+}
+
+.CodeMirror-wrap .CodeMirror-scroll {
+ overflow-x: hidden;
+}
+
+.CodeMirror-measure {
+ position: absolute;
+ width: 100%; height: 0px;
+ overflow: hidden;
+ visibility: hidden;
+}
+.CodeMirror-measure pre { position: static; }
+
+.CodeMirror div.CodeMirror-cursor {
+ position: absolute;
+ visibility: hidden;
+ border-right: none;
+ width: 0;
+}
+.CodeMirror-focused div.CodeMirror-cursor {
+ visibility: visible;
+}
+
+.CodeMirror-selected { background: #d9d9d9; }
+.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
+
+.cm-searching {
+ background: #ffa;
+ background: rgba(255, 255, 0, .4);
+}
+
+/* IE7 hack to prevent it from returning funny offsetTops on the spans */
+.CodeMirror span { *vertical-align: text-bottom; }
+
+@media print {
+ /* Hide the cursor when printing */
+ .CodeMirror div.CodeMirror-cursor {
+ visibility: hidden;
+ }
+}
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/codemirror.min.css b/plugins/jetpack/modules/custom-css/custom-css/css/codemirror.min.css
new file mode 100644
index 00000000..92af420a
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/codemirror.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.rtl .CodeMirror{direction:ltr}.CodeMirror{font-family:monospace;height:400px}.CodeMirror-scroll{overflow:auto}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999}.CodeMirror div.CodeMirror-cursor{border-left:1px solid #000;z-index:3}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor{width:auto;border:0;background:#7e7;z-index:1}.cm-tab{display:inline-block}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable{color:#000}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-property{color:#000}.cm-s-default .cm-operator{color:#000}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-error{color:red}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-invalidchar{color:red}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{line-height:1;position:relative;overflow:hidden;background:#fff;color:#000}.CodeMirror-scroll{margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;padding-right:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;padding-bottom:30px;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;padding-bottom:30px;margin-bottom:-32px;display:inline-block}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text}.CodeMirror pre{border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-code pre{border-right:30px solid transparent;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.CodeMirror-wrap .CodeMirror-code pre{border-right:none;width:auto}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-wrap .CodeMirror-scroll{overflow-x:hidden}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-measure pre{position:static}.CodeMirror div.CodeMirror-cursor{position:absolute;visibility:hidden;border-right:none;width:0}.CodeMirror-focused div.CodeMirror-cursor{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}@media print{.CodeMirror div.CodeMirror-cursor{visibility:hidden}} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/css-editor-rtl.css b/plugins/jetpack/modules/custom-css/custom-css/css/css-editor-rtl.css
new file mode 100644
index 00000000..a8202fb2
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/css-editor-rtl.css
@@ -0,0 +1,33 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#revision-field-post_title, #revision-field-post_excerpt {
+ display: none;
+}
+
+#safecssform {
+ position: relative;
+}
+
+#poststuff {
+ padding-top: 0;
+}
+
+#safecss {
+ min-height: 250px;
+ width: 100%;
+}
+
+.misc-pub-section > span {
+ font-weight: bold;
+}
+
+.misc-pub-section > div {
+ margin-top: 3px;
+}
+#safecss-ace .ace_gutter {
+ z-index: 1;
+}
+
+#post-body-content{
+ margin-bottom: 20px;
+}
+
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/css-editor-rtl.min.css b/plugins/jetpack/modules/custom-css/custom-css/css/css-editor-rtl.min.css
new file mode 100644
index 00000000..62eb4809
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/css-editor-rtl.min.css
@@ -0,0 +1 @@
+#revision-field-post_excerpt,#revision-field-post_title{display:none}#safecssform{position:relative}#poststuff{padding-top:0}#safecss{min-height:250px;width:100%}.misc-pub-section>span{font-weight:700}.misc-pub-section>div{margin-top:3px}#safecss-ace .ace_gutter{z-index:1}#post-body-content{margin-bottom:20px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/css-editor.css b/plugins/jetpack/modules/custom-css/custom-css/css/css-editor.css
new file mode 100644
index 00000000..f85bec14
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/css-editor.css
@@ -0,0 +1,32 @@
+#revision-field-post_title, #revision-field-post_excerpt {
+ display: none;
+}
+
+#safecssform {
+ position: relative;
+}
+
+#poststuff {
+ padding-top: 0;
+}
+
+#safecss {
+ min-height: 250px;
+ width: 100%;
+}
+
+.misc-pub-section > span {
+ font-weight: bold;
+}
+
+.misc-pub-section > div {
+ margin-top: 3px;
+}
+#safecss-ace .ace_gutter {
+ z-index: 1;
+}
+
+#post-body-content{
+ margin-bottom: 20px;
+}
+
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/css-editor.min.css b/plugins/jetpack/modules/custom-css/custom-css/css/css-editor.min.css
new file mode 100644
index 00000000..6245c019
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/css-editor.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#revision-field-post_excerpt,#revision-field-post_title{display:none}#safecssform{position:relative}#poststuff{padding-top:0}#safecss{min-height:250px;width:100%}.misc-pub-section>span{font-weight:700}.misc-pub-section>div{margin-top:3px}#safecss-ace .ace_gutter{z-index:1}#post-body-content{margin-bottom:20px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/customizer-control.css b/plugins/jetpack/modules/custom-css/custom-css/css/customizer-control.css
new file mode 100644
index 00000000..e7927ff2
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/customizer-control.css
@@ -0,0 +1,150 @@
+/* NOAUTORTL */
+.for-codemirror, .CodeMirror {
+ font-family: Consolas, Monaco, monospace;
+ font-size: 12px;
+ line-height: 16px;
+ margin: 0;
+ direction: ltr;
+ text-align: left;
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+}
+#customize-control-custom_css#customize-control-custom_css {
+ margin-right: -12px;
+ margin-left: -12px;
+ width: calc( 100% + 24px );
+}
+.customize-control-code_editor .CodeMirror {
+ height: 300px;
+ height: calc( 100vh - 268px );
+}
+.for-codemirror {
+ width: 98%;
+ height: 300px;
+}
+#customize-control-wpcom_custom_css_content_width_control {
+ position: relative;
+}
+#customize-control-wpcom_custom_css_content_width_control > label {
+ position: relative;
+ width: 100px;
+}
+#customize-control-wpcom_custom_css_content_width_control .customize-control-title {
+ padding-bottom: 6px;
+}
+#customize-controls #customize-control-wpcom_custom_css_content_width_control input[type="text"], /* stronger selector to override new-customizer.css */
+#customize-control-wpcom_custom_css_content_width_control input[type="text"] {
+ width: 64px;
+ padding-right: 22px;
+ text-align: right;
+}
+#customize-control-wpcom_custom_css_content_width_control input[type="text"] + span {
+ position: absolute;
+ left: 43px;
+ padding-top: 3px;
+ opacity: .8;
+}
+@-moz-document url-prefix() {
+ #customize-control-wpcom_custom_css_content_width_control input[type="text"] + span {
+ top: 47px;
+ }
+}
+#customize-control-wpcom_custom_css_content_width_control input[type="text"]:focus + span {
+ opacity: 1;
+}
+
+#customize-control-wpcom_custom_css_content_width_control .description {
+ display: block;
+ margin: 28px 0 0 0;
+ color: #aaa;
+}
+
+#customize-control-wpcom_custom_css_content_width_control .description strong {
+ font-style: normal;
+}
+
+#customize-control-jetpack_custom_css_control {
+ position: relative;
+}
+
+.css-help {
+ border-bottom: 1px solid #ddd;
+ background: #ffffff;
+ position: relative;
+ right: 0;
+ left: 0;
+ width: 100%;
+ padding: 0;
+ overflow: hidden;
+}
+
+.css-help a {
+ float: none;
+ display: inline-block;
+ text-decoration: none;
+ border-bottom: 4px solid transparent;
+ color: #555d66;
+ padding: 7px 10px 5px;
+ transition: .15s color ease-in-out,.15s background-color ease-in-out,.15s border-color ease-in-out;
+}
+
+.css-help a:hover {
+ color: #0073aa;
+ background-color: #f3f3f5;
+}
+
+.css-help a:before {
+ display: inline-block;
+ position: relative;
+ font-family: dashicons;
+ font-size: 20px;
+ padding-right: 3px;
+ top: 5px;
+ line-height: 1px;
+}
+
+.css-help a:focus {
+ color: #0073aa;
+ background-color: #f3f3f5;
+ border-bottom-color: #0073aa;
+ box-shadow: none;
+}
+
+.css-help a#revisions-link:before {
+ content: "\f321";
+}
+
+.css-help a#help-link:before {
+ content: "\f223";
+}
+
+#sub-accordion-section-custom_css .customize-control {
+ margin: 12px 0;
+}
+
+#sub-accordion-section-custom_css .customize-control-jetpackCss {
+ margin: 0 -12px;
+ width: calc( 100% + 24px );
+}
+
+#customize-theme-controls #sub-accordion-section-custom_css .customize-control-title {
+ margin-left: 0;
+ margin-right: 0;
+}
+
+#sub-accordion-section-custom_css #customize-control-jetpack_css_preprocessors_control select {
+ max-width: 75%;
+}
+
+body.editing-css .wp-full-overlay-sidebar {
+ width: 500px;
+}
+
+body.editing-css .wp-full-overlay.expanded {
+ margin-left: 500px;
+}
+
+input[type=jetpackCss] {
+ display: none;
+}
+
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/rtl/codemirror-rtl.css b/plugins/jetpack/modules/custom-css/custom-css/css/rtl/codemirror-rtl.css
new file mode 100644
index 00000000..74ad41fb
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/rtl/codemirror-rtl.css
@@ -0,0 +1,260 @@
+/* This file was automatically generated on Sep 10 2013 23:18:59 */
+
+/* BASICS */
+.CodeMirror {
+ /* Set height, width, borders, and global font properties here */
+ font-family: monospace;
+ height: 300px;
+}
+.CodeMirror-scroll {
+ /* Set scrolling behavior here */
+ overflow: auto;
+}
+
+/* PADDING */
+
+.CodeMirror-lines {
+ padding: 4px 0; /* Vertical padding around content */
+}
+.CodeMirror pre {
+ padding: 0 4px; /* Horizontal padding of content */
+}
+
+.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ background-color: white; /* The little square between H and V scrollbars */
+}
+
+/* GUTTER */
+
+.CodeMirror-gutters {
+ border-left: 1px solid #ddd;
+ background-color: #f7f7f7;
+ white-space: nowrap;
+}
+.CodeMirror-linenumbers {}
+.CodeMirror-linenumber {
+ padding: 0 5px 0 3px;
+ min-width: 20px;
+ text-align: left;
+ color: #999;
+}
+
+/* CURSOR */
+
+.CodeMirror div.CodeMirror-cursor {
+ border-right: 1px solid black;
+ z-index: 3;
+}
+/* Shown when moving in bi-directional text */
+.CodeMirror div.CodeMirror-secondarycursor {
+ border-right: 1px solid silver;
+}
+.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
+ width: auto;
+ border: 0;
+ background: #7e7;
+ z-index: 1;
+}
+/* Can style cursor different in overwrite (non-insert) mode */
+.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {}
+
+.cm-tab { display: inline-block; }
+
+/* DEFAULT THEME */
+
+.cm-s-default .cm-keyword {color: #708;}
+.cm-s-default .cm-atom {color: #219;}
+.cm-s-default .cm-number {color: #164;}
+.cm-s-default .cm-def {color: #00f;}
+.cm-s-default .cm-variable {color: black;}
+.cm-s-default .cm-variable-2 {color: #05a;}
+.cm-s-default .cm-variable-3 {color: #085;}
+.cm-s-default .cm-property {color: black;}
+.cm-s-default .cm-operator {color: black;}
+.cm-s-default .cm-comment {color: #a50;}
+.cm-s-default .cm-string {color: #a11;}
+.cm-s-default .cm-string-2 {color: #f50;}
+.cm-s-default .cm-meta {color: #555;}
+.cm-s-default .cm-error {color: #f00;}
+.cm-s-default .cm-qualifier {color: #555;}
+.cm-s-default .cm-builtin {color: #30a;}
+.cm-s-default .cm-bracket {color: #997;}
+.cm-s-default .cm-tag {color: #170;}
+.cm-s-default .cm-attribute {color: #00c;}
+.cm-s-default .cm-header {color: blue;}
+.cm-s-default .cm-quote {color: #090;}
+.cm-s-default .cm-hr {color: #999;}
+.cm-s-default .cm-link {color: #00c;}
+
+.cm-negative {color: #d44;}
+.cm-positive {color: #292;}
+.cm-header, .cm-strong {font-weight: bold;}
+.cm-em {font-style: italic;}
+.cm-link {text-decoration: underline;}
+
+.cm-invalidchar {color: #f00;}
+
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
+.CodeMirror-activeline-background {background: #e8f2ff;}
+
+/* STOP */
+
+/* The rest of this file contains styles related to the mechanics of
+ the editor. You probably shouldn't touch them. */
+
+.CodeMirror {
+ line-height: 1;
+ position: relative;
+ overflow: hidden;
+ background: white;
+ color: black;
+}
+
+.CodeMirror-scroll {
+ /* 30px is the magic margin used to hide the element's real scrollbars */
+ /* See overflow: hidden in .CodeMirror */
+ margin-bottom: -30px; margin-left: -30px;
+ padding-bottom: 30px; padding-left: 30px;
+ height: 100%;
+ outline: none; /* Prevent dragging from highlighting the element */
+ position: relative;
+}
+.CodeMirror-sizer {
+ position: relative;
+}
+
+/* The fake, visible scrollbars. Used to force redraw during scrolling
+ before actuall scrolling happens, thus preventing shaking and
+ flickering artifacts. */
+.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ position: absolute;
+ z-index: 6;
+ display: none;
+}
+.CodeMirror-vscrollbar {
+ left: 0; top: 0;
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+.CodeMirror-hscrollbar {
+ bottom: 0; right: 0;
+ overflow-y: hidden;
+ overflow-x: scroll;
+}
+.CodeMirror-scrollbar-filler {
+ left: 0; bottom: 0;
+}
+.CodeMirror-gutter-filler {
+ right: 0; bottom: 0;
+}
+
+.CodeMirror-gutters {
+ position: absolute; right: 0; top: 0;
+ padding-bottom: 30px;
+ z-index: 3;
+}
+.CodeMirror-gutter {
+ white-space: normal;
+ height: 100%;
+ padding-bottom: 30px;
+ margin-bottom: -32px;
+ display: inline-block;
+ /* Hack to make IE7 behave */
+ *zoom:1;
+ *display:inline;
+}
+.CodeMirror-gutter-elt {
+ position: absolute;
+ cursor: default;
+ z-index: 4;
+}
+
+.CodeMirror-lines {
+ cursor: text;
+}
+.CodeMirror pre {
+ /* Reset some styles that the rest of the page might have set */
+ -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
+ border-width: 0;
+ background: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ margin: 0;
+ white-space: pre;
+ word-wrap: normal;
+ line-height: inherit;
+ color: inherit;
+ z-index: 2;
+ position: relative;
+ overflow: visible;
+}
+.CodeMirror-wrap pre {
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ word-break: normal;
+}
+.CodeMirror-code pre {
+ border-left: 30px solid transparent;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+}
+.CodeMirror-wrap .CodeMirror-code pre {
+ border-left: none;
+ width: auto;
+}
+.CodeMirror-linebackground {
+ position: absolute;
+ right: 0; left: 0; top: 0; bottom: 0;
+ z-index: 0;
+}
+
+.CodeMirror-linewidget {
+ position: relative;
+ z-index: 2;
+ overflow: auto;
+}
+
+.CodeMirror-widget {
+}
+
+.CodeMirror-wrap .CodeMirror-scroll {
+ overflow-x: hidden;
+}
+
+.CodeMirror-measure {
+ position: absolute;
+ width: 100%; height: 0px;
+ overflow: hidden;
+ visibility: hidden;
+}
+.CodeMirror-measure pre { position: static; }
+
+.CodeMirror div.CodeMirror-cursor {
+ position: absolute;
+ visibility: hidden;
+ border-left: none;
+ width: 0;
+}
+.CodeMirror-focused div.CodeMirror-cursor {
+ visibility: visible;
+}
+
+.CodeMirror-selected { background: #d9d9d9; }
+.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
+
+.cm-searching {
+ background: #ffa;
+ background: rgba(255, 255, 0, .4);
+}
+
+/* IE7 hack to prevent it from returning funny offsetTops on the spans */
+.CodeMirror span { *vertical-align: text-bottom; }
+
+@media print {
+ /* Hide the cursor when printing */
+ .CodeMirror div.CodeMirror-cursor {
+ visibility: hidden;
+ }
+}
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror-rtl.css b/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror-rtl.css
new file mode 100644
index 00000000..649fb168
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror-rtl.css
@@ -0,0 +1,7 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.CodeMirror, #safecss {
+ font-family: Consolas, Monaco, monospace;
+ font-size: 12px;
+ line-height: 16px;
+ min-height: 300px;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror-rtl.min.css b/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror-rtl.min.css
new file mode 100644
index 00000000..8dce7dda
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror-rtl.min.css
@@ -0,0 +1 @@
+#safecss,.CodeMirror{font-family:Consolas,Monaco,monospace;font-size:12px;line-height:16px;min-height:300px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror.css b/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror.css
new file mode 100644
index 00000000..924bb4d0
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror.css
@@ -0,0 +1,6 @@
+.CodeMirror, #safecss {
+ font-family: Consolas, Monaco, monospace;
+ font-size: 12px;
+ line-height: 16px;
+ min-height: 300px;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror.min.css b/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror.min.css
new file mode 100644
index 00000000..024ae478
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/css/use-codemirror.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#safecss,.CodeMirror{font-family:Consolas,Monaco,monospace;font-size:12px;line-height:16px;min-height:300px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/js/codemirror.min.js b/plugins/jetpack/modules/custom-css/custom-css/js/codemirror.min.js
new file mode 100644
index 00000000..efc8a0fb
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/js/codemirror.min.js
@@ -0,0 +1,11 @@
+/**
+ * http://codemirror.net/
+ * MIT License
+ * Includes CSS & LESS modes.
+ * v3.19.1
+ */
+window.CodeMirror=function(){"use strict";function x(a,c){if(!(this instanceof x))return new x(a,c);this.options=c=c||{};for(var d in _c)!c.hasOwnProperty(d)&&_c.hasOwnProperty(d)&&(c[d]=_c[d]);J(c);var e="string"==typeof c.value?0:c.value.first,f=this.display=y(a,e);f.wrapper.CodeMirror=this,G(this),c.autofocus&&!p&&Nb(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,focused:!1,suppressEdits:!1,pasteIncoming:!1,draggingText:!1,highlight:new Xe},E(this),c.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap");var g=c.value;"string"==typeof g&&(g=new ge(c.value,c.mode)),Fb(this,ke)(this,g),b&&setTimeout(ff(Mb,this,!0),20),Pb(this);var h;try{h=document.activeElement==f.input}catch(i){}h||c.autofocus&&!p?setTimeout(ff(mc,this),20):nc(this),Fb(this,function(){for(var a in $c)$c.propertyIsEnumerable(a)&&$c[a](this,c[a],bd);for(var b=0;b<fd.length;++b)fd[b](this)})()}function y(a,b){var d={},e=d.input=lf("textarea",null,null,"position: absolute; padding: 0; width: 1px; height: 1em; outline: none; font-size: 4px;");return f?e.style.width="1000px":e.setAttribute("wrap","off"),o&&(e.style.border="1px solid black"),e.setAttribute("autocorrect","off"),e.setAttribute("autocapitalize","off"),e.setAttribute("spellcheck","false"),d.inputDiv=lf("div",[e],null,"overflow: hidden; position: relative; width: 3px; height: 0px;"),d.scrollbarH=lf("div",[lf("div",null,null,"height: 1px")],"CodeMirror-hscrollbar"),d.scrollbarV=lf("div",[lf("div",null,null,"width: 1px")],"CodeMirror-vscrollbar"),d.scrollbarFiller=lf("div",null,"CodeMirror-scrollbar-filler"),d.gutterFiller=lf("div",null,"CodeMirror-gutter-filler"),d.lineDiv=lf("div",null,"CodeMirror-code"),d.selectionDiv=lf("div",null,null,"position: relative; z-index: 1"),d.cursor=lf("div","\xa0","CodeMirror-cursor"),d.otherCursor=lf("div","\xa0","CodeMirror-cursor CodeMirror-secondarycursor"),d.measure=lf("div",null,"CodeMirror-measure"),d.lineSpace=lf("div",[d.measure,d.selectionDiv,d.lineDiv,d.cursor,d.otherCursor],null,"position: relative; outline: none"),d.mover=lf("div",[lf("div",[d.lineSpace],"CodeMirror-lines")],null,"position: relative"),d.sizer=lf("div",[d.mover],"CodeMirror-sizer"),d.heightForcer=lf("div",null,null,"position: absolute; height: "+Ve+"px; width: 1px;"),d.gutters=lf("div",null,"CodeMirror-gutters"),d.lineGutter=null,d.scroller=lf("div",[d.sizer,d.heightForcer,d.gutters],"CodeMirror-scroll"),d.scroller.setAttribute("tabIndex","-1"),d.wrapper=lf("div",[d.inputDiv,d.scrollbarH,d.scrollbarV,d.scrollbarFiller,d.gutterFiller,d.scroller],"CodeMirror"),c&&(d.gutters.style.zIndex=-1,d.scroller.style.paddingRight=0),a.appendChild?a.appendChild(d.wrapper):a(d.wrapper),o&&(e.style.width="0px"),f||(d.scroller.draggable=!0),k?(d.inputDiv.style.height="1px",d.inputDiv.style.position="absolute"):c&&(d.scrollbarH.style.minWidth=d.scrollbarV.style.minWidth="18px"),d.viewOffset=d.lastSizeC=0,d.showingFrom=d.showingTo=b,d.lineNumWidth=d.lineNumInnerWidth=d.lineNumChars=null,d.prevInput="",d.alignWidgets=!1,d.pollingFast=!1,d.poll=new Xe,d.cachedCharWidth=d.cachedTextHeight=null,d.measureLineCache=[],d.measureLineCachePos=0,d.inaccurateSelection=!1,d.maxLine=null,d.maxLineLength=0,d.maxLineChanged=!1,d.wheelDX=d.wheelDY=d.wheelStartX=d.wheelStartY=null,d}function z(a){a.doc.mode=x.getMode(a.options,a.doc.modeOption),a.doc.iter(function(a){a.stateAfter&&(a.stateAfter=null),a.styles&&(a.styles=null)}),a.doc.frontier=a.doc.first,bb(a,100),a.state.modeGen++,a.curOp&&Ib(a)}function A(a){a.options.lineWrapping?(a.display.wrapper.className+=" CodeMirror-wrap",a.display.sizer.style.minWidth=""):(a.display.wrapper.className=a.display.wrapper.className.replace(" CodeMirror-wrap",""),I(a)),C(a),Ib(a),pb(a),setTimeout(function(){K(a)},100)}function B(a){var b=Ab(a.display),c=a.options.lineWrapping,d=c&&Math.max(5,a.display.scroller.clientWidth/Bb(a.display)-3);return function(e){return Gd(a.doc,e)?0:c?(Math.ceil(e.text.length/d)||1)*b:b}}function C(a){var b=a.doc,c=B(a);b.iter(function(a){var b=c(a);b!=a.height&&oe(a,b)})}function D(a){var b=kd[a.options.keyMap],c=b.style;a.display.wrapper.className=a.display.wrapper.className.replace(/\s*cm-keymap-\S+/g,"")+(c?" cm-keymap-"+c:""),a.state.disableInput=b.disableInput}function E(a){a.display.wrapper.className=a.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+a.options.theme.replace(/(^|\s)\s*/g," cm-s-"),pb(a)}function F(a){G(a),Ib(a),setTimeout(function(){M(a)},20)}function G(a){var b=a.display.gutters,c=a.options.gutters;mf(b);for(var d=0;d<c.length;++d){var e=c[d],f=b.appendChild(lf("div",null,"CodeMirror-gutter "+e));"CodeMirror-linenumbers"==e&&(a.display.lineGutter=f,f.style.width=(a.display.lineNumWidth||1)+"px")}b.style.display=d?"":"none"}function H(a,b){if(0==b.height)return 0;for(var d,c=b.text.length,e=b;d=Dd(e);){var f=d.find();e=le(a,f.from.line),c+=f.from.ch-f.to.ch}for(e=b;d=Ed(e);){var f=d.find();c-=e.text.length-f.from.ch,e=le(a,f.to.line),c+=e.text.length-f.to.ch}return c}function I(a){var b=a.display,c=a.doc;b.maxLine=le(c,c.first),b.maxLineLength=H(c,b.maxLine),b.maxLineChanged=!0,c.iter(function(a){var d=H(c,a);d>b.maxLineLength&&(b.maxLineLength=d,b.maxLine=a)})}function J(a){var b=bf(a.gutters,"CodeMirror-linenumbers");-1==b&&a.lineNumbers?a.gutters=a.gutters.concat(["CodeMirror-linenumbers"]):b>-1&&!a.lineNumbers&&(a.gutters=a.gutters.slice(0),a.gutters.splice(b,1))}function K(a){var b=a.display,c=a.doc.height,d=c+gb(b);b.sizer.style.minHeight=b.heightForcer.style.top=d+"px",b.gutters.style.height=Math.max(d,b.scroller.clientHeight-Ve)+"px";var e=Math.max(d,b.scroller.scrollHeight),f=b.scroller.scrollWidth>b.scroller.clientWidth+1,g=e>b.scroller.clientHeight+1;g?(b.scrollbarV.style.display="block",b.scrollbarV.style.bottom=f?tf(b.measure)+"px":"0",b.scrollbarV.firstChild.style.height=e-b.scroller.clientHeight+b.scrollbarV.clientHeight+"px"):(b.scrollbarV.style.display="",b.scrollbarV.firstChild.style.height="0"),f?(b.scrollbarH.style.display="block",b.scrollbarH.style.right=g?tf(b.measure)+"px":"0",b.scrollbarH.firstChild.style.width=b.scroller.scrollWidth-b.scroller.clientWidth+b.scrollbarH.clientWidth+"px"):(b.scrollbarH.style.display="",b.scrollbarH.firstChild.style.width="0"),f&&g?(b.scrollbarFiller.style.display="block",b.scrollbarFiller.style.height=b.scrollbarFiller.style.width=tf(b.measure)+"px"):b.scrollbarFiller.style.display="",f&&a.options.coverGutterNextToScrollbar&&a.options.fixedGutter?(b.gutterFiller.style.display="block",b.gutterFiller.style.height=tf(b.measure)+"px",b.gutterFiller.style.width=b.gutters.offsetWidth+"px"):b.gutterFiller.style.display="",l&&0===tf(b.measure)&&(b.scrollbarV.style.minWidth=b.scrollbarH.style.minHeight=m?"18px":"12px",b.scrollbarV.style.pointerEvents=b.scrollbarH.style.pointerEvents="none")}function L(a,b,c){var d=a.scroller.scrollTop,e=a.wrapper.clientHeight;"number"==typeof c?d=c:c&&(d=c.top,e=c.bottom-c.top),d=Math.floor(d-fb(a));var f=Math.ceil(d+e);return{from:qe(b,d),to:qe(b,f)}}function M(a){var b=a.display;if(b.alignWidgets||b.gutters.firstChild&&a.options.fixedGutter){for(var c=P(b)-b.scroller.scrollLeft+a.doc.scrollLeft,d=b.gutters.offsetWidth,e=c+"px",f=b.lineDiv.firstChild;f;f=f.nextSibling)if(f.alignable)for(var g=0,h=f.alignable;g<h.length;++g)h[g].style.left=e;a.options.fixedGutter&&(b.gutters.style.left=c+d+"px")}}function N(a){if(!a.options.lineNumbers)return!1;var b=a.doc,c=O(a.options,b.first+b.size-1),d=a.display;if(c.length!=d.lineNumChars){var e=d.measure.appendChild(lf("div",[lf("div",c)],"CodeMirror-linenumber CodeMirror-gutter-elt")),f=e.firstChild.offsetWidth,g=e.offsetWidth-f;return d.lineGutter.style.width="",d.lineNumInnerWidth=Math.max(f,d.lineGutter.offsetWidth-g),d.lineNumWidth=d.lineNumInnerWidth+g,d.lineNumChars=d.lineNumInnerWidth?c.length:-1,d.lineGutter.style.width=d.lineNumWidth+"px",!0}return!1}function O(a,b){return String(a.lineNumberFormatter(b+a.firstLineNumber))}function P(a){return pf(a.scroller).left-pf(a.sizer).left}function Q(a,b,c,d){for(var g,e=a.display.showingFrom,f=a.display.showingTo,h=L(a.display,a.doc,c),i=!0;;i=!1){var j=a.display.scroller.clientWidth;if(!R(a,b,h,d))break;if(g=!0,b=[],Z(a),K(a),i&&a.options.lineWrapping&&j!=a.display.scroller.clientWidth)d=!0;else if(d=!1,c&&(c=Math.min(a.display.scroller.scrollHeight-a.display.scroller.clientHeight,"number"==typeof c?c:c.top)),h=L(a.display,a.doc,c),h.from>=a.display.showingFrom&&h.to<=a.display.showingTo)break}return g&&(Qe(a,"update",a),(a.display.showingFrom!=e||a.display.showingTo!=f)&&Qe(a,"viewportChange",a,a.display.showingFrom,a.display.showingTo)),g}function R(a,b,c,d){var e=a.display,f=a.doc;if(!e.wrapper.clientWidth)return e.showingFrom=e.showingTo=f.first,e.viewOffset=0,void 0;if(!(!d&&0==b.length&&c.from>e.showingFrom&&c.to<e.showingTo)){N(a)&&(b=[{from:f.first,to:f.first+f.size}]);var g=e.sizer.style.marginLeft=e.gutters.offsetWidth+"px";e.scrollbarH.style.left=a.options.fixedGutter?g:"0";var h=1/0;if(a.options.lineNumbers)for(var i=0;i<b.length;++i)b[i].diff&&b[i].from<h&&(h=b[i].from);var j=f.first+f.size,k=Math.max(c.from-a.options.viewportMargin,f.first),l=Math.min(j,c.to+a.options.viewportMargin);if(e.showingFrom<k&&k-e.showingFrom<20&&(k=Math.max(f.first,e.showingFrom)),e.showingTo>l&&e.showingTo-l<20&&(l=Math.min(j,e.showingTo)),w)for(k=pe(Fd(f,le(f,k)));j>l&&Gd(f,le(f,l));)++l;var m=[{from:Math.max(e.showingFrom,f.first),to:Math.min(e.showingTo,j)}];if(m=m[0].from>=m[0].to?[]:U(m,b),w)for(var i=0;i<m.length;++i)for(var o,n=m[i];o=Ed(le(f,n.to-1));){var p=o.find().from.line;if(!(p>n.from)){m.splice(i--,1);break}n.to=p}for(var q=0,i=0;i<m.length;++i){var n=m[i];n.from<k&&(n.from=k),n.to>l&&(n.to=l),n.from>=n.to?m.splice(i--,1):q+=n.to-n.from}if(!d&&q==l-k&&k==e.showingFrom&&l==e.showingTo)return T(a),void 0;m.sort(function(a,b){return a.from-b.from});try{var r=document.activeElement}catch(s){}.7*(l-k)>q&&(e.lineDiv.style.display="none"),W(a,k,l,m,h),e.lineDiv.style.display="",r&&document.activeElement!=r&&r.offsetHeight&&r.focus();var t=k!=e.showingFrom||l!=e.showingTo||e.lastSizeC!=e.wrapper.clientHeight;return t&&(e.lastSizeC=e.wrapper.clientHeight,bb(a,400)),e.showingFrom=k,e.showingTo=l,S(a),T(a),!0}}function S(a){for(var f,b=a.display,d=b.lineDiv.offsetTop,e=b.lineDiv.firstChild;e;e=e.nextSibling)if(e.lineObj){if(c){var g=e.offsetTop+e.offsetHeight;f=g-d,d=g}else{var h=pf(e);f=h.bottom-h.top}var i=e.lineObj.height-f;if(2>f&&(f=Ab(b)),i>.001||-.001>i){oe(e.lineObj,f);var j=e.lineObj.widgets;if(j)for(var k=0;k<j.length;++k)j[k].height=j[k].node.offsetHeight}}}function T(a){var b=a.display.viewOffset=re(a,le(a.doc,a.display.showingFrom));a.display.mover.style.top=b+"px"}function U(a,b){for(var c=0,d=b.length||0;d>c;++c){for(var e=b[c],f=[],g=e.diff||0,h=0,i=a.length;i>h;++h){var j=a[h];e.to<=j.from&&e.diff?f.push({from:j.from+g,to:j.to+g}):e.to<=j.from||e.from>=j.to?f.push(j):(e.from>j.from&&f.push({from:j.from,to:e.from}),e.to<j.to&&f.push({from:e.to+g,to:j.to+g}))}a=f}return a}function V(a){for(var b=a.display,c={},d={},e=b.gutters.firstChild,f=0;e;e=e.nextSibling,++f)c[a.options.gutters[f]]=e.offsetLeft,d[a.options.gutters[f]]=e.offsetWidth;return{fixedPos:P(b),gutterTotalWidth:b.gutters.offsetWidth,gutterLeft:c,gutterWidth:d,wrapperWidth:b.wrapper.clientWidth}}function W(a,b,c,d,e){function l(b){var c=b.nextSibling;return f&&q&&a.display.currentWheelTarget==b?(b.style.display="none",b.lineObj=null):b.parentNode.removeChild(b),c}var g=V(a),h=a.display,i=a.options.lineNumbers;d.length||f&&a.display.currentWheelTarget||mf(h.lineDiv);var j=h.lineDiv,k=j.firstChild,m=d.shift(),n=b;for(a.doc.iter(b,c,function(b){if(m&&m.to==n&&(m=d.shift()),Gd(a.doc,b)){if(0!=b.height&&oe(b,0),b.widgets&&k&&k.previousSibling)for(var c=0;c<b.widgets.length;++c){var f=b.widgets[c];if(f.showIfHidden){var h=k.previousSibling;if(/pre/i.test(h.nodeName)){var o=lf("div",null,null,"position: relative");h.parentNode.replaceChild(o,h),o.appendChild(h),h=o}var p=h.appendChild(lf("div",[f.node],"CodeMirror-linewidget"));f.handleMouseEvents||(p.ignoreEvents=!0),Y(f,p,h,g)}}}else if(m&&m.from<=n&&m.to>n){for(;k.lineObj!=b;)k=l(k);i&&n>=e&&k.lineNumber&&of(k.lineNumber,O(a.options,n)),k=k.nextSibling}else{if(b.widgets)for(var s,q=0,r=k;r&&20>q;++q,r=r.nextSibling)if(r.lineObj==b&&/div/i.test(r.nodeName)){s=r;break}var t=X(a,b,n,g,s);if(t!=s)j.insertBefore(t,k);else{for(;k!=s;)k=l(k);k=k.nextSibling}t.lineObj=b}++n});k;)k=l(k)}function X(a,b,d,e,f){var k,g=Xd(a,b),h=g.pre,i=b.gutterMarkers,j=a.display,l=g.bgClass?g.bgClass+" "+(b.bgClass||""):b.bgClass;if(!(a.options.lineNumbers||i||l||b.wrapClass||b.widgets))return h;if(f){f.alignable=null;for(var q,m=!0,n=0,o=null,p=f.firstChild;p;p=q)if(q=p.nextSibling,/\bCodeMirror-linewidget\b/.test(p.className)){for(var r=0;r<b.widgets.length;++r){var s=b.widgets[r];if(s.node==p.firstChild){s.above||o||(o=p),Y(s,p,f,e),++n;break}}if(r==b.widgets.length){m=!1;break}}else f.removeChild(p);f.insertBefore(h,o),m&&n==b.widgets.length&&(k=f,f.className=b.wrapClass||"")}if(k||(k=lf("div",null,b.wrapClass,"position: relative"),k.appendChild(h)),l&&k.insertBefore(lf("div",null,l+" CodeMirror-linebackground"),k.firstChild),a.options.lineNumbers||i){var t=k.insertBefore(lf("div",null,null,"position: absolute; left: "+(a.options.fixedGutter?e.fixedPos:-e.gutterTotalWidth)+"px"),k.firstChild);if(a.options.fixedGutter&&(k.alignable||(k.alignable=[])).push(t),!a.options.lineNumbers||i&&i["CodeMirror-linenumbers"]||(k.lineNumber=t.appendChild(lf("div",O(a.options,d),"CodeMirror-linenumber CodeMirror-gutter-elt","left: "+e.gutterLeft["CodeMirror-linenumbers"]+"px; width: "+j.lineNumInnerWidth+"px"))),i)for(var u=0;u<a.options.gutters.length;++u){var v=a.options.gutters[u],w=i.hasOwnProperty(v)&&i[v];w&&t.appendChild(lf("div",[w],"CodeMirror-gutter-elt","left: "+e.gutterLeft[v]+"px; width: "+e.gutterWidth[v]+"px"))}}if(c&&(k.style.zIndex=2),b.widgets&&k!=f)for(var r=0,x=b.widgets;r<x.length;++r){var s=x[r],y=lf("div",[s.node],"CodeMirror-linewidget");s.handleMouseEvents||(y.ignoreEvents=!0),Y(s,y,k,e),s.above?k.insertBefore(y,a.options.lineNumbers&&0!=b.height?t:h):k.appendChild(y),Qe(s,"redraw")}return k}function Y(a,b,c,d){if(a.noHScroll){(c.alignable||(c.alignable=[])).push(b);var e=d.wrapperWidth;b.style.left=d.fixedPos+"px",a.coverGutter||(e-=d.gutterTotalWidth,b.style.paddingLeft=d.gutterTotalWidth+"px"),b.style.width=e+"px"}a.coverGutter&&(b.style.zIndex=5,b.style.position="relative",a.noHScroll||(b.style.marginLeft=-d.gutterTotalWidth+"px"))}function Z(a){var b=a.display,c=Cc(a.doc.sel.from,a.doc.sel.to);if(c||a.options.showCursorWhenSelecting?$(a):b.cursor.style.display=b.otherCursor.style.display="none",c?b.selectionDiv.style.display="none":_(a),a.options.moveInputWithCursor){var d=vb(a,a.doc.sel.head,"div"),e=pf(b.wrapper),f=pf(b.lineDiv);b.inputDiv.style.top=Math.max(0,Math.min(b.wrapper.clientHeight-10,d.top+f.top-e.top))+"px",b.inputDiv.style.left=Math.max(0,Math.min(b.wrapper.clientWidth-10,d.left+f.left-e.left))+"px"}}function $(a){var b=a.display,c=vb(a,a.doc.sel.head,"div");b.cursor.style.left=c.left+"px",b.cursor.style.top=c.top+"px",b.cursor.style.height=Math.max(0,c.bottom-c.top)*a.options.cursorHeight+"px",b.cursor.style.display="",c.other?(b.otherCursor.style.display="",b.otherCursor.style.left=c.other.left+"px",b.otherCursor.style.top=c.other.top+"px",b.otherCursor.style.height=.85*(c.other.bottom-c.other.top)+"px"):b.otherCursor.style.display="none"}function _(a){function h(a,b,c,d){0>b&&(b=0),e.appendChild(lf("div",null,"CodeMirror-selected","position: absolute; left: "+a+"px; top: "+b+"px; width: "+(null==c?f-a:c)+"px; height: "+(d-b)+"px"))}function i(b,d,e){function m(c,d){return ub(a,Bc(b,c),"div",i,d)}var k,l,i=le(c,b),j=i.text.length;return Af(se(i),d||0,null==e?j:e,function(a,b,c){var n,o,p,i=m(a,"left");if(a==b)n=i,o=p=i.left;else{if(n=m(b-1,"right"),"rtl"==c){var q=i;i=n,n=q}o=i.left,p=n.right}null==d&&0==a&&(o=g),n.top-i.top>3&&(h(o,i.top,null,i.bottom),o=g,i.bottom<n.top&&h(o,i.bottom,null,n.top)),null==e&&b==j&&(p=f),(!k||i.top<k.top||i.top==k.top&&i.left<k.left)&&(k=i),(!l||n.bottom>l.bottom||n.bottom==l.bottom&&n.right>l.right)&&(l=n),g+1>o&&(o=g),h(o,n.top,p-o,n.bottom)}),{start:k,end:l}}var b=a.display,c=a.doc,d=a.doc.sel,e=document.createDocumentFragment(),f=b.lineSpace.offsetWidth,g=hb(a.display);if(d.from.line==d.to.line)i(d.from.line,d.from.ch,d.to.ch);else{var j=le(c,d.from.line),k=le(c,d.to.line),l=Fd(c,j)==Fd(c,k),m=i(d.from.line,d.from.ch,l?j.text.length:null).end,n=i(d.to.line,l?0:null,d.to.ch).start;l&&(m.top<n.top-2?(h(m.right,m.top,null,m.bottom),h(g,n.top,n.left,n.bottom)):h(m.right,m.top,n.left-m.right,m.bottom)),m.bottom<n.top&&h(g,m.bottom,null,n.top)}nf(b.selectionDiv,e),b.selectionDiv.style.display=""}function ab(a){if(a.state.focused){var b=a.display;clearInterval(b.blinker);var c=!0;b.cursor.style.visibility=b.otherCursor.style.visibility="",a.options.cursorBlinkRate>0&&(b.blinker=setInterval(function(){b.cursor.style.visibility=b.otherCursor.style.visibility=(c=!c)?"":"hidden"},a.options.cursorBlinkRate))}}function bb(a,b){a.doc.mode.startState&&a.doc.frontier<a.display.showingTo&&a.state.highlight.set(b,ff(cb,a))}function cb(a){var b=a.doc;if(b.frontier<b.first&&(b.frontier=b.first),!(b.frontier>=a.display.showingTo)){var f,c=+new Date+a.options.workTime,d=hd(b.mode,eb(a,b.frontier)),e=[];b.iter(b.frontier,Math.min(b.first+b.size,a.display.showingTo+500),function(g){if(b.frontier>=a.display.showingFrom){var h=g.styles;g.styles=Sd(a,g,d);for(var i=!h||h.length!=g.styles.length,j=0;!i&&j<h.length;++j)i=h[j]!=g.styles[j];i&&(f&&f.end==b.frontier?f.end++:e.push(f={start:b.frontier,end:b.frontier+1})),g.stateAfter=hd(b.mode,d)}else Ud(a,g,d),g.stateAfter=0==b.frontier%5?hd(b.mode,d):null;return++b.frontier,+new Date>c?(bb(a,a.options.workDelay),!0):void 0}),e.length&&Fb(a,function(){for(var a=0;a<e.length;++a)Ib(this,e[a].start,e[a].end)})()}}function db(a,b,c){for(var d,e,f=a.doc,g=c?-1:b-(a.doc.mode.innerMode?1e3:100),h=b;h>g;--h){if(h<=f.first)return f.first;var i=le(f,h-1);if(i.stateAfter&&(!c||h<=f.frontier))return h;var j=Ye(i.text,null,a.options.tabSize);(null==e||d>j)&&(e=h-1,d=j)}return e}function eb(a,b,c){var d=a.doc,e=a.display;if(!d.mode.startState)return!0;var f=db(a,b,c),g=f>d.first&&le(d,f-1).stateAfter;return g=g?hd(d.mode,g):id(d.mode),d.iter(f,b,function(c){Ud(a,c,g);var h=f==b-1||0==f%5||f>=e.showingFrom&&f<e.showingTo;c.stateAfter=h?hd(d.mode,g):null,++f}),c&&(d.frontier=f),g}function fb(a){return a.lineSpace.offsetTop}function gb(a){return a.mover.offsetHeight-a.lineSpace.offsetHeight}function hb(a){var b=nf(a.measure,lf("pre",null,null,"text-align: left")).appendChild(lf("span","x"));return b.offsetLeft}function ib(a,b,c,d,e){var f=-1;if(d=d||lb(a,b),d.crude){var g=d.left+c*d.width;return{left:g,right:g+d.width,top:d.top,bottom:d.bottom}}for(var h=c;;h+=f){var i=d[h];if(i)break;0>f&&0==h&&(f=1)}return e=h>c?"left":c>h?"right":e,"left"==e&&i.leftSide?i=i.leftSide:"right"==e&&i.rightSide&&(i=i.rightSide),{left:c>h?i.right:i.left,right:h>c?i.left:i.right,top:i.top,bottom:i.bottom}}function jb(a,b){for(var c=a.display.measureLineCache,d=0;d<c.length;++d){var e=c[d];if(e.text==b.text&&e.markedSpans==b.markedSpans&&a.display.scroller.clientWidth==e.width&&e.classes==b.textClass+"|"+b.wrapClass)return e}}function kb(a,b){var c=jb(a,b);c&&(c.text=c.measure=c.markedSpans=null)}function lb(a,b){var c=jb(a,b);if(c)return c.measure;var d=mb(a,b),e=a.display.measureLineCache,f={text:b.text,width:a.display.scroller.clientWidth,markedSpans:b.markedSpans,measure:d,classes:b.textClass+"|"+b.wrapClass};return 16==e.length?e[++a.display.measureLineCachePos%16]=f:e.push(f),d}function mb(a,e){function t(a){var b=a.top-p.top,c=a.bottom-p.top;c>s&&(c=s),0>b&&(b=0);for(var d=q.length-2;d>=0;d-=2){var e=q[d],f=q[d+1];if(!(e>c||b>f)&&(b>=e&&f>=c||e>=b&&c>=f||Math.min(c,f)-Math.max(b,e)>=c-b>>1)){q[d]=Math.min(b,e),q[d+1]=Math.max(c,f);break}}return 0>d&&(d=q.length,q.push(b,c)),{left:a.left-p.left,right:a.right-p.left,top:d,bottom:null}}function u(a){a.bottom=q[a.top+1],a.top=q[a.top]}if(!a.options.lineWrapping&&e.text.length>=a.options.crudeMeasuringFrom)return nb(a,e);var f=a.display,g=ef(e.text.length),h=Xd(a,e,g,!0).pre;if(b&&!c&&!a.options.lineWrapping&&h.childNodes.length>100){for(var i=document.createDocumentFragment(),j=10,k=h.childNodes.length,l=0,m=Math.ceil(k/j);m>l;++l){for(var n=lf("div",null,null,"display: inline-block"),o=0;j>o&&k;++o)n.appendChild(h.firstChild),--k;i.appendChild(n)}h.appendChild(i)}nf(f.measure,h);var p=pf(f.lineDiv),q=[],r=ef(e.text.length),s=h.offsetHeight;d&&f.measure.first!=h&&nf(f.measure,h);for(var v,l=0;l<g.length;++l)if(v=g[l]){var w=v,x=null;if(/\bCodeMirror-widget\b/.test(v.className)&&v.getClientRects){1==v.firstChild.nodeType&&(w=v.firstChild);var y=w.getClientRects();y.length>1&&(x=r[l]=t(y[0]),x.rightSide=t(y[y.length-1]))}x||(x=r[l]=t(pf(w))),v.measureRight&&(x.right=pf(v.measureRight).left),v.leftSide&&(x.leftSide=t(pf(v.leftSide)))}mf(a.display.measure);for(var v,l=0;l<r.length;++l)(v=r[l])&&(u(v),v.leftSide&&u(v.leftSide),v.rightSide&&u(v.rightSide));return r}function nb(a,b){var c=new Od(b.text.slice(0,100),null);b.textClass&&(c.textClass=b.textClass);var d=mb(a,c),e=ib(a,c,0,d,"left"),f=ib(a,c,99,d,"right");return{crude:!0,top:e.top,left:e.left,bottom:e.bottom,width:(f.right-e.left)/100}}function ob(a,b){var c=!1;if(b.markedSpans)for(var d=0;d<b.markedSpans;++d){var e=b.markedSpans[d];!e.collapsed||null!=e.to&&e.to!=b.text.length||(c=!0)}var f=!c&&jb(a,b);if(f||b.text.length>=a.options.crudeMeasuringFrom)return ib(a,b,b.text.length,f&&f.measure,"right").right;var g=Xd(a,b,null,!0).pre,h=g.appendChild(vf(a.display.measure));return nf(a.display.measure,g),pf(h).right-pf(a.display.lineDiv).left}function pb(a){a.display.measureLineCache.length=a.display.measureLineCachePos=0,a.display.cachedCharWidth=a.display.cachedTextHeight=null,a.options.lineWrapping||(a.display.maxLineChanged=!0),a.display.lineNumChars=null}function qb(){return window.pageXOffset||(document.documentElement||document.body).scrollLeft}function rb(){return window.pageYOffset||(document.documentElement||document.body).scrollTop}function sb(a,b,c,d){if(b.widgets)for(var e=0;e<b.widgets.length;++e)if(b.widgets[e].above){var f=Md(b.widgets[e]);c.top+=f,c.bottom+=f}if("line"==d)return c;d||(d="local");var g=re(a,b);if("local"==d?g+=fb(a.display):g-=a.display.viewOffset,"page"==d||"window"==d){var h=pf(a.display.lineSpace);g+=h.top+("window"==d?0:rb());var i=h.left+("window"==d?0:qb());c.left+=i,c.right+=i}return c.top+=g,c.bottom+=g,c}function tb(a,b,c){if("div"==c)return b;var d=b.left,e=b.top;if("page"==c)d-=qb(),e-=rb();else if("local"==c||!c){var f=pf(a.display.sizer);d+=f.left,e+=f.top}var g=pf(a.display.lineSpace);return{left:d-g.left,top:e-g.top}}function ub(a,b,c,d,e){return d||(d=le(a.doc,b.line)),sb(a,d,ib(a,d,b.ch,null,e),c)}function vb(a,b,c,d,e){function f(b,f){var g=ib(a,d,b,e,f?"right":"left");return f?g.left=g.right:g.right=g.left,sb(a,d,g,c)}function g(a,b){var c=h[b],d=c.level%2;return a==Bf(c)&&b&&c.level<h[b-1].level?(c=h[--b],a=Cf(c)-(c.level%2?0:1),d=!0):a==Cf(c)&&b<h.length-1&&c.level<h[b+1].level&&(c=h[++b],a=Bf(c)-c.level%2,d=!1),d&&a==c.to&&a>c.from?f(a-1):f(a,d)}d=d||le(a.doc,b.line),e||(e=lb(a,d));var h=se(d),i=b.ch;if(!h)return f(i);var j=Jf(h,i),k=g(i,j);return null!=If&&(k.other=g(i,If)),k}function wb(a,b,c,d){var e=new Bc(a,b);return e.xRel=d,c&&(e.outside=!0),e}function xb(a,b,c){var d=a.doc;if(c+=a.display.viewOffset,0>c)return wb(d.first,0,!0,-1);var e=qe(d,c),f=d.first+d.size-1;if(e>f)return wb(d.first+d.size-1,le(d,f).text.length,!0,1);for(0>b&&(b=0);;){var g=le(d,e),h=yb(a,g,e,b,c),i=Ed(g),j=i&&i.find();if(!i||!(h.ch>j.from.ch||h.ch==j.from.ch&&h.xRel>0))return h;e=j.to.line}}function yb(a,b,c,d,e){function j(d){var e=vb(a,Bc(c,d),"line",b,i);return g=!0,f>e.bottom?e.left-h:f<e.top?e.left+h:(g=!1,e.left)}var f=e-re(a,b),g=!1,h=2*a.display.wrapper.clientWidth,i=lb(a,b),k=se(b),l=b.text.length,m=Df(b),n=Ef(b),o=j(m),p=g,q=j(n),r=g;if(d>q)return wb(c,n,r,1);for(;;){if(k?n==m||n==Lf(b,m,1):1>=n-m){for(var s=o>d||q-d>=d-o?m:n,t=d-(s==m?o:q);kf.test(b.text.charAt(s));)++s;var u=wb(c,s,s==m?p:r,0>t?-1:t?1:0);return u}var v=Math.ceil(l/2),w=m+v;if(k){w=m;for(var x=0;v>x;++x)w=Lf(b,w,1)}var y=j(w);y>d?(n=w,q=y,(r=g)&&(q+=1e3),l=v):(m=w,o=y,p=g,l-=v)}}function Ab(a){if(null!=a.cachedTextHeight)return a.cachedTextHeight;if(null==zb){zb=lf("pre");for(var b=0;49>b;++b)zb.appendChild(document.createTextNode("x")),zb.appendChild(lf("br"));zb.appendChild(document.createTextNode("x"))}nf(a.measure,zb);var c=zb.offsetHeight/50;return c>3&&(a.cachedTextHeight=c),mf(a.measure),c||1}function Bb(a){if(null!=a.cachedCharWidth)return a.cachedCharWidth;var b=lf("span","x"),c=lf("pre",[b]);nf(a.measure,c);var d=b.offsetWidth;return d>2&&(a.cachedCharWidth=d),d||10}function Db(a){a.curOp={changes:[],forceUpdate:!1,updateInput:null,userSelChange:null,textChanged:null,selectionChanged:!1,cursorActivity:!1,updateMaxLine:!1,updateScrollPos:!1,id:++Cb},Pe++||(Oe=[])}function Eb(a){var b=a.curOp,c=a.doc,d=a.display;if(a.curOp=null,b.updateMaxLine&&I(a),d.maxLineChanged&&!a.options.lineWrapping&&d.maxLine){var e=ob(a,d.maxLine);d.sizer.style.minWidth=Math.max(0,e+3+Ve)+"px",d.maxLineChanged=!1;var f=Math.max(0,d.sizer.offsetLeft+d.sizer.offsetWidth-d.scroller.clientWidth);f<c.scrollLeft&&!b.updateScrollPos&&ac(a,Math.min(d.scroller.scrollLeft,f),!0)}var g,h;if(b.updateScrollPos)g=b.updateScrollPos;else if(b.selectionChanged&&d.scroller.clientHeight){var i=vb(a,c.sel.head);g=Rc(a,i.left,i.top,i.left,i.bottom)}(b.changes.length||b.forceUpdate||g&&null!=g.scrollTop)&&(h=Q(a,b.changes,g&&g.scrollTop,b.forceUpdate),a.display.scroller.offsetHeight&&(a.doc.scrollTop=a.display.scroller.scrollTop)),!h&&b.selectionChanged&&Z(a),b.updateScrollPos?(d.scroller.scrollTop=d.scrollbarV.scrollTop=c.scrollTop=g.scrollTop,d.scroller.scrollLeft=d.scrollbarH.scrollLeft=c.scrollLeft=g.scrollLeft,M(a),b.scrollToPos&&Pc(a,Gc(a.doc,b.scrollToPos.from),Gc(a.doc,b.scrollToPos.to),b.scrollToPos.margin)):g&&Oc(a),b.selectionChanged&&ab(a),a.state.focused&&b.updateInput&&Mb(a,b.userSelChange);var j=b.maybeHiddenMarkers,k=b.maybeUnhiddenMarkers;if(j)for(var l=0;l<j.length;++l)j[l].lines.length||Ne(j[l],"hide");if(k)for(var l=0;l<k.length;++l)k[l].lines.length&&Ne(k[l],"unhide");var m;if(--Pe||(m=Oe,Oe=null),b.textChanged&&Ne(a,"change",a,b.textChanged),b.cursorActivity&&Ne(a,"cursorActivity",a),m)for(var l=0;l<m.length;++l)m[l]()}function Fb(a,b){return function(){var c=a||this,d=!c.curOp;d&&Db(c);try{var e=b.apply(c,arguments)}finally{d&&Eb(c)}return e}}function Gb(a){return function(){var c,b=this.cm&&!this.cm.curOp;b&&Db(this.cm);try{c=a.apply(this,arguments)}finally{b&&Eb(this.cm)}return c}}function Hb(a,b){var d,c=!a.curOp;c&&Db(a);try{d=b()}finally{c&&Eb(a)}return d}function Ib(a,b,c,d){null==b&&(b=a.doc.first),null==c&&(c=a.doc.first+a.doc.size),a.curOp.changes.push({from:b,to:c,diff:d})}function Jb(a){a.display.pollingFast||a.display.poll.set(a.options.pollInterval,function(){Lb(a),a.state.focused&&Jb(a)})}function Kb(a){function c(){var d=Lb(a);d||b?(a.display.pollingFast=!1,Jb(a)):(b=!0,a.display.poll.set(60,c))}var b=!1;a.display.pollingFast=!0,a.display.poll.set(20,c)}function Lb(a){var c=a.display.input,e=a.display.prevInput,f=a.doc,g=f.sel;if(!a.state.focused||xf(c)||Ob(a)||a.state.disableInput)return!1;a.state.pasteIncoming&&a.state.fakedLastChar&&(c.value=c.value.substring(0,c.value.length-1),a.state.fakedLastChar=!1);var h=c.value;if(h==e&&Cc(g.from,g.to))return!1;if(b&&!d&&a.display.inputHasSelection===h)return Mb(a,!0),!1;var i=!a.curOp;i&&Db(a),g.shift=!1;for(var j=0,k=Math.min(e.length,h.length);k>j&&e.charCodeAt(j)==h.charCodeAt(j);)++j;var l=g.from,m=g.to;j<e.length?l=Bc(l.line,l.ch-(e.length-j)):a.state.overwrite&&Cc(l,m)&&!a.state.pasteIncoming&&(m=Bc(m.line,Math.min(le(f,m.line).text.length,m.ch+(h.length-j))));var n=a.curOp.updateInput,o={from:l,to:m,text:wf(h.slice(j)),origin:a.state.pasteIncoming?"paste":"+input"};return uc(a.doc,o,"end"),a.curOp.updateInput=n,Qe(a,"inputRead",a,o),h.length>1e3||h.indexOf("\n")>-1?c.value=a.display.prevInput="":a.display.prevInput=h,i&&Eb(a),a.state.pasteIncoming=!1,!0}function Mb(a,c){var e,f,g=a.doc;if(Cc(g.sel.from,g.sel.to))c&&(a.display.prevInput=a.display.input.value="",b&&!d&&(a.display.inputHasSelection=null));else{a.display.prevInput="",e=yf&&(g.sel.to.line-g.sel.from.line>100||(f=a.getSelection()).length>1e3);var h=e?"-":f||a.getSelection();a.display.input.value=h,a.state.focused&&af(a.display.input),b&&!d&&(a.display.inputHasSelection=h)}a.display.inaccurateSelection=e}function Nb(a){"nocursor"==a.options.readOnly||p&&document.activeElement==a.display.input||a.display.input.focus()}function Ob(a){return a.options.readOnly||a.doc.cantEdit}function Pb(a){function e(){a.state.focused&&setTimeout(ff(Nb,a),0)}function h(){null==g&&(g=setTimeout(function(){g=null,c.cachedCharWidth=c.cachedTextHeight=sf=null,pb(a),Hb(a,ff(Ib,a))},100))}function i(){for(var a=c.wrapper.parentNode;a&&a!=document.body;a=a.parentNode);a?setTimeout(i,5e3):Me(window,"resize",h)}function j(b){Re(a,b)||a.options.onDragEvent&&a.options.onDragEvent(a,Ee(b))||Ie(b)}function l(){c.inaccurateSelection&&(c.prevInput="",c.inaccurateSelection=!1,c.input.value=a.getSelection(),af(c.input))}var c=a.display;Le(c.scroller,"mousedown",Fb(a,Ub)),b?Le(c.scroller,"dblclick",Fb(a,function(b){if(!Re(a,b)){var c=Rb(a,b);if(c&&!Xb(a,b)&&!Qb(a.display,b)){Fe(b);var d=Yc(le(a.doc,c.line).text,c);Jc(a.doc,d.from,d.to)}}})):Le(c.scroller,"dblclick",function(b){Re(a,b)||Fe(b)}),Le(c.lineSpace,"selectstart",function(a){Qb(c,a)||Fe(a)}),u||Le(c.scroller,"contextmenu",function(b){pc(a,b)}),Le(c.scroller,"scroll",function(){c.scroller.clientHeight&&(_b(a,c.scroller.scrollTop),ac(a,c.scroller.scrollLeft,!0),Ne(a,"scroll",a))}),Le(c.scrollbarV,"scroll",function(){c.scroller.clientHeight&&_b(a,c.scrollbarV.scrollTop)}),Le(c.scrollbarH,"scroll",function(){c.scroller.clientHeight&&ac(a,c.scrollbarH.scrollLeft)}),Le(c.scroller,"mousewheel",function(b){dc(a,b)}),Le(c.scroller,"DOMMouseScroll",function(b){dc(a,b)}),Le(c.scrollbarH,"mousedown",e),Le(c.scrollbarV,"mousedown",e),Le(c.wrapper,"scroll",function(){c.wrapper.scrollTop=c.wrapper.scrollLeft=0});var g;Le(window,"resize",h),setTimeout(i,5e3),Le(c.input,"keyup",Fb(a,function(b){Re(a,b)||a.options.onKeyEvent&&a.options.onKeyEvent(a,Ee(b))||16==b.keyCode&&(a.doc.sel.shift=!1)})),Le(c.input,"input",function(){b&&!d&&a.display.inputHasSelection&&(a.display.inputHasSelection=null),Kb(a)}),Le(c.input,"keydown",Fb(a,kc)),Le(c.input,"keypress",Fb(a,lc)),Le(c.input,"focus",ff(mc,a)),Le(c.input,"blur",ff(nc,a)),a.options.dragDrop&&(Le(c.scroller,"dragstart",function(b){$b(a,b)}),Le(c.scroller,"dragenter",j),Le(c.scroller,"dragover",j),Le(c.scroller,"drop",Fb(a,Zb))),Le(c.scroller,"paste",function(b){Qb(c,b)||(Nb(a),Kb(a))}),Le(c.input,"paste",function(){if(f&&!a.state.fakedLastChar&&!(new Date-a.state.lastMiddleDown<200)){var b=c.input.selectionStart,d=c.input.selectionEnd;c.input.value+="$",c.input.selectionStart=b,c.input.selectionEnd=d,a.state.fakedLastChar=!0}a.state.pasteIncoming=!0,Kb(a)}),Le(c.input,"cut",l),Le(c.input,"copy",l),k&&Le(c.sizer,"mouseup",function(){document.activeElement==c.input&&c.input.blur(),Nb(a)})}function Qb(a,b){for(var c=Je(b);c!=a.wrapper;c=c.parentNode)if(!c||c.ignoreEvents||c.parentNode==a.sizer&&c!=a.mover)return!0}function Rb(a,b,c){var d=a.display;if(!c){var e=Je(b);if(e==d.scrollbarH||e==d.scrollbarH.firstChild||e==d.scrollbarV||e==d.scrollbarV.firstChild||e==d.scrollbarFiller||e==d.gutterFiller)return null}var f,g,h=pf(d.lineSpace);try{f=b.clientX,g=b.clientY}catch(b){return null}return xb(a,f-h.left,g-h.top)}function Ub(a){function q(a){if(!Cc(p,a)){if(p=a,"single"==j)return Jc(c.doc,Gc(e,h),a),void 0;if(n=Gc(e,n),o=Gc(e,o),"double"==j){var b=Yc(le(e,a.line).text,a);Dc(a,n)?Jc(c.doc,b.from,o):Jc(c.doc,n,b.to)
+}else"triple"==j&&(Dc(a,n)?Jc(c.doc,o,Gc(e,Bc(a.line,0))):Jc(c.doc,n,Gc(e,Bc(a.line+1,0))))}}function t(a){var b=++s,f=Rb(c,a,!0);if(f)if(Cc(f,l)){var h=a.clientY<r.top?-20:a.clientY>r.bottom?20:0;h&&setTimeout(Fb(c,function(){s==b&&(d.scroller.scrollTop+=h,t(a))}),50)}else{c.state.focused||mc(c),l=f,q(f);var g=L(d,e);(f.line>=g.to||f.line<g.from)&&setTimeout(Fb(c,function(){s==b&&t(a)}),150)}}function v(a){s=1/0,Fe(a),Nb(c),Me(document,"mousemove",w),Me(document,"mouseup",x)}if(!Re(this,a)){var c=this,d=c.display,e=c.doc,g=e.sel;if(g.shift=a.shiftKey,Qb(d,a))return f||(d.scroller.draggable=!1,setTimeout(function(){d.scroller.draggable=!0},100)),void 0;if(!Xb(c,a)){var h=Rb(c,a);switch(Ke(a)){case 3:return u&&pc.call(c,c,a),void 0;case 2:return f&&(c.state.lastMiddleDown=+new Date),h&&Jc(c.doc,h),setTimeout(ff(Nb,c),20),Fe(a),void 0}if(!h)return Je(a)==d.scroller&&Fe(a),void 0;c.state.focused||mc(c);var i=+new Date,j="single";if(Tb&&Tb.time>i-400&&Cc(Tb.pos,h))j="triple",Fe(a),setTimeout(ff(Nb,c),20),Zc(c,h.line);else if(Sb&&Sb.time>i-400&&Cc(Sb.pos,h)){j="double",Tb={time:i,pos:h},Fe(a);var k=Yc(le(e,h.line).text,h);Jc(c.doc,k.from,k.to)}else Sb={time:i,pos:h};var l=h;if(c.options.dragDrop&&qf&&!Ob(c)&&!Cc(g.from,g.to)&&!Dc(h,g.from)&&!Dc(g.to,h)&&"single"==j){var m=Fb(c,function(b){f&&(d.scroller.draggable=!1),c.state.draggingText=!1,Me(document,"mouseup",m),Me(d.scroller,"drop",m),Math.abs(a.clientX-b.clientX)+Math.abs(a.clientY-b.clientY)<10&&(Fe(b),Jc(c.doc,h),Nb(c))});return f&&(d.scroller.draggable=!0),c.state.draggingText=m,d.scroller.dragDrop&&d.scroller.dragDrop(),Le(document,"mouseup",m),Le(d.scroller,"drop",m),void 0}Fe(a),"single"==j&&Jc(c.doc,Gc(e,h));var n=g.from,o=g.to,p=h,r=pf(d.wrapper),s=0,w=Fb(c,function(a){b||Ke(a)?t(a):v(a)}),x=Fb(c,v);Le(document,"mousemove",w),Le(document,"mouseup",x)}}}function Vb(a,b,c,d,e){try{var f=b.clientX,g=b.clientY}catch(b){return!1}if(f>=Math.floor(pf(a.display.gutters).right))return!1;d&&Fe(b);var h=a.display,i=pf(h.lineDiv);if(g>i.bottom||!Te(a,c))return He(b);g-=i.top-h.viewOffset;for(var j=0;j<a.options.gutters.length;++j){var k=h.gutters.childNodes[j];if(k&&pf(k).right>=f){var l=qe(a.doc,g),m=a.options.gutters[j];return e(a,c,a,l,m,b),He(b)}}}function Wb(a,b){return Te(a,"gutterContextMenu")?Vb(a,b,"gutterContextMenu",!1,Ne):!1}function Xb(a,b){return Vb(a,b,"gutterClick",!0,Qe)}function Zb(a){var c=this;if(!(Re(c,a)||Qb(c.display,a)||c.options.onDragEvent&&c.options.onDragEvent(c,Ee(a)))){Fe(a),b&&(Yb=+new Date);var d=Rb(c,a,!0),e=a.dataTransfer.files;if(d&&!Ob(c))if(e&&e.length&&window.FileReader&&window.File)for(var f=e.length,g=Array(f),h=0,i=function(a,b){var e=new FileReader;e.onload=function(){g[b]=e.result,++h==f&&(d=Gc(c.doc,d),uc(c.doc,{from:d,to:d,text:wf(g.join("\n")),origin:"paste"},"around"))},e.readAsText(a)},j=0;f>j;++j)i(e[j],j);else{if(c.state.draggingText&&!Dc(d,c.doc.sel.from)&&!Dc(c.doc.sel.to,d))return c.state.draggingText(a),setTimeout(ff(Nb,c),20),void 0;try{var g=a.dataTransfer.getData("Text");if(g){var k=c.doc.sel.from,l=c.doc.sel.to;Lc(c.doc,d,d),c.state.draggingText&&Ac(c.doc,"",k,l,"paste"),c.replaceSelection(g,null,"paste"),Nb(c),mc(c)}}catch(a){}}}}function $b(a,c){if(b&&(!a.state.draggingText||+new Date-Yb<100))return Ie(c),void 0;if(!Re(a,c)&&!Qb(a.display,c)){var d=a.getSelection();if(c.dataTransfer.setData("Text",d),c.dataTransfer.setDragImage&&!j){var e=lf("img",null,null,"position: fixed; left: 0; top: 0;");e.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",i&&(e.width=e.height=1,a.display.wrapper.appendChild(e),e._top=e.offsetTop),c.dataTransfer.setDragImage(e,0,0),i&&e.parentNode.removeChild(e)}}}function _b(b,c){Math.abs(b.doc.scrollTop-c)<2||(b.doc.scrollTop=c,a||Q(b,[],c),b.display.scroller.scrollTop!=c&&(b.display.scroller.scrollTop=c),b.display.scrollbarV.scrollTop!=c&&(b.display.scrollbarV.scrollTop=c),a&&Q(b,[]),bb(b,100))}function ac(a,b,c){(c?b==a.doc.scrollLeft:Math.abs(a.doc.scrollLeft-b)<2)||(b=Math.min(b,a.display.scroller.scrollWidth-a.display.scroller.clientWidth),a.doc.scrollLeft=b,M(a),a.display.scroller.scrollLeft!=b&&(a.display.scroller.scrollLeft=b),a.display.scrollbarH.scrollLeft!=b&&(a.display.scrollbarH.scrollLeft=b))}function dc(b,c){var d=c.wheelDeltaX,e=c.wheelDeltaY;null==d&&c.detail&&c.axis==c.HORIZONTAL_AXIS&&(d=c.detail),null==e&&c.detail&&c.axis==c.VERTICAL_AXIS?e=c.detail:null==e&&(e=c.wheelDelta);var g=b.display,h=g.scroller;if(d&&h.scrollWidth>h.clientWidth||e&&h.scrollHeight>h.clientHeight){if(e&&q&&f)for(var j=c.target;j!=h;j=j.parentNode)if(j.lineObj){b.display.currentWheelTarget=j;break}if(d&&!a&&!i&&null!=cc)return e&&_b(b,Math.max(0,Math.min(h.scrollTop+e*cc,h.scrollHeight-h.clientHeight))),ac(b,Math.max(0,Math.min(h.scrollLeft+d*cc,h.scrollWidth-h.clientWidth))),Fe(c),g.wheelStartX=null,void 0;if(e&&null!=cc){var k=e*cc,l=b.doc.scrollTop,m=l+g.wrapper.clientHeight;0>k?l=Math.max(0,l+k-50):m=Math.min(b.doc.height,m+k+50),Q(b,[],{top:l,bottom:m})}20>bc&&(null==g.wheelStartX?(g.wheelStartX=h.scrollLeft,g.wheelStartY=h.scrollTop,g.wheelDX=d,g.wheelDY=e,setTimeout(function(){if(null!=g.wheelStartX){var a=h.scrollLeft-g.wheelStartX,b=h.scrollTop-g.wheelStartY,c=b&&g.wheelDY&&b/g.wheelDY||a&&g.wheelDX&&a/g.wheelDX;g.wheelStartX=g.wheelStartY=null,c&&(cc=(cc*bc+c)/(bc+1),++bc)}},200)):(g.wheelDX+=d,g.wheelDY+=e))}}function ec(a,b,c){if("string"==typeof b&&(b=jd[b],!b))return!1;a.display.pollingFast&&Lb(a)&&(a.display.pollingFast=!1);var d=a.doc,e=d.sel.shift,f=!1;try{Ob(a)&&(a.state.suppressEdits=!0),c&&(d.sel.shift=!1),f=b(a)!=We}finally{d.sel.shift=e,a.state.suppressEdits=!1}return f}function fc(a){var b=a.state.keyMaps.slice(0);return a.options.extraKeys&&b.push(a.options.extraKeys),b.push(a.options.keyMap),b}function hc(a,b){var c=ld(a.options.keyMap),e=c.auto;clearTimeout(gc),e&&!nd(b)&&(gc=setTimeout(function(){ld(a.options.keyMap)==c&&(a.options.keyMap=e.call?e.call(null,a):e,D(a))},50));var f=od(b,!0),g=!1;if(!f)return!1;var h=fc(a);return g=b.shiftKey?md("Shift-"+f,h,function(b){return ec(a,b,!0)})||md(f,h,function(b){return("string"==typeof b?/^go[A-Z]/.test(b):b.motion)?ec(a,b):void 0}):md(f,h,function(b){return ec(a,b)}),g&&(Fe(b),ab(a),d&&(b.oldKeyCode=b.keyCode,b.keyCode=0),Qe(a,"keyHandled",a,f,b)),g}function ic(a,b,c){var d=md("'"+c+"'",fc(a),function(b){return ec(a,b,!0)});return d&&(Fe(b),ab(a),Qe(a,"keyHandled",a,"'"+c+"'",b)),d}function kc(a){var c=this;if(c.state.focused||mc(c),!(Re(c,a)||c.options.onKeyEvent&&c.options.onKeyEvent(c,Ee(a)))){b&&27==a.keyCode&&(a.returnValue=!1);var d=a.keyCode;c.doc.sel.shift=16==d||a.shiftKey;var e=hc(c,a);i&&(jc=e?d:null,!e&&88==d&&!yf&&(q?a.metaKey:a.ctrlKey)&&c.replaceSelection(""))}}function lc(a){var c=this;if(!(Re(c,a)||c.options.onKeyEvent&&c.options.onKeyEvent(c,Ee(a)))){var e=a.keyCode,f=a.charCode;if(i&&e==jc)return jc=null,Fe(a),void 0;if(!(i&&(!a.which||a.which<10)||k)||!hc(c,a)){var g=String.fromCharCode(null==f?e:f);this.options.electricChars&&this.doc.mode.electricChars&&this.options.smartIndent&&!Ob(this)&&this.doc.mode.electricChars.indexOf(g)>-1&&setTimeout(Fb(c,function(){Uc(c,c.doc.sel.to.line,"smart")}),75),ic(c,a,g)||(b&&!d&&(c.display.inputHasSelection=null),Kb(c))}}}function mc(a){"nocursor"!=a.options.readOnly&&(a.state.focused||(Ne(a,"focus",a),a.state.focused=!0,-1==a.display.wrapper.className.search(/\bCodeMirror-focused\b/)&&(a.display.wrapper.className+=" CodeMirror-focused"),a.curOp||(Mb(a,!0),f&&setTimeout(ff(Mb,a,!0),0))),Jb(a),ab(a))}function nc(a){a.state.focused&&(Ne(a,"blur",a),a.state.focused=!1,a.display.wrapper.className=a.display.wrapper.className.replace(" CodeMirror-focused","")),clearInterval(a.display.blinker),setTimeout(function(){a.state.focused||(a.doc.sel.shift=!1)},150)}function pc(a,c){function l(){if(null!=e.input.selectionStart){var a=e.input.value="\u200b"+(Cc(f.from,f.to)?"":e.input.value);e.prevInput="\u200b",e.input.selectionStart=1,e.input.selectionEnd=a.length}}function m(){if(e.inputDiv.style.position="relative",e.input.style.cssText=k,d&&(e.scrollbarV.scrollTop=e.scroller.scrollTop=h),Jb(a),null!=e.input.selectionStart){(!b||d)&&l(),clearTimeout(oc);var c=0,f=function(){" "==e.prevInput&&0==e.input.selectionStart?Fb(a,jd.selectAll)(a):c++<10?oc=setTimeout(f,500):Mb(a)};oc=setTimeout(f,200)}}if(!Re(a,c,"contextmenu")){var e=a.display,f=a.doc.sel;if(!Qb(e,c)&&!Wb(a,c)){var g=Rb(a,c),h=e.scroller.scrollTop;if(g&&!i){var j=a.options.resetSelectionOnContextMenu;j&&(Cc(f.from,f.to)||Dc(g,f.from)||!Dc(g,f.to))&&Fb(a,Lc)(a.doc,g,g);var k=e.input.style.cssText;if(e.inputDiv.style.position="absolute",e.input.style.cssText="position: fixed; width: 30px; height: 30px; top: "+(c.clientY-5)+"px; left: "+(c.clientX-5)+"px; z-index: 1000; background: white; outline: none;"+"border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);",Nb(a),Mb(a,!0),Cc(f.from,f.to)&&(e.input.value=e.prevInput=" "),b&&!d&&l(),u){Ie(c);var n=function(){Me(window,"mouseup",n),setTimeout(m,20)};Le(window,"mouseup",n)}else setTimeout(m,50)}}}}function rc(a,b,c){if(!Dc(b.from,c))return Gc(a,c);var d=b.text.length-1-(b.to.line-b.from.line);if(c.line>b.to.line+d){var e=c.line-d,f=a.first+a.size-1;return e>f?Bc(f,le(a,f).text.length):Hc(c,le(a,e).text.length)}if(c.line==b.to.line+d)return Hc(c,_e(b.text).length+(1==b.text.length?b.from.ch:0)+le(a,b.to.line).text.length-b.to.ch);var g=c.line-b.from.line;return Hc(c,b.text[g].length+(g?0:b.from.ch))}function sc(a,b,c){if(c&&"object"==typeof c)return{anchor:rc(a,b,c.anchor),head:rc(a,b,c.head)};if("start"==c)return{anchor:b.from,head:b.from};var d=qc(b);if("around"==c)return{anchor:b.from,head:d};if("end"==c)return{anchor:d,head:d};var e=function(a){if(Dc(a,b.from))return a;if(!Dc(b.to,a))return d;var c=a.line+b.text.length-(b.to.line-b.from.line)-1,e=a.ch;return a.line==b.to.line&&(e+=d.ch-b.to.ch),Bc(c,e)};return{anchor:e(a.sel.anchor),head:e(a.sel.head)}}function tc(a,b,c){var d={canceled:!1,from:b.from,to:b.to,text:b.text,origin:b.origin,cancel:function(){this.canceled=!0}};return c&&(d.update=function(b,c,d,e){b&&(this.from=Gc(a,b)),c&&(this.to=Gc(a,c)),d&&(this.text=d),void 0!==e&&(this.origin=e)}),Ne(a,"beforeChange",a,d),a.cm&&Ne(a.cm,"beforeChange",a.cm,d),d.canceled?null:{from:d.from,to:d.to,text:d.text,origin:d.origin}}function uc(a,b,c,d){if(a.cm){if(!a.cm.curOp)return Fb(a.cm,uc)(a,b,c,d);if(a.cm.state.suppressEdits)return}if(!(Te(a,"beforeChange")||a.cm&&Te(a.cm,"beforeChange"))||(b=tc(a,b,!0))){var e=v&&!d&&Bd(a,b.from,b.to);if(e){for(var f=e.length-1;f>=1;--f)vc(a,{from:e[f].from,to:e[f].to,text:[""]});e.length&&vc(a,{from:e[0].from,to:e[0].to,text:b.text},c)}else vc(a,b,c)}}function vc(a,b,c){if(1!=b.text.length||""!=b.text[0]||!Cc(b.from,b.to)){var d=sc(a,b,c);we(a,b,d,a.cm?a.cm.curOp.id:0/0),yc(a,b,d,zd(a,b));var e=[];je(a,function(a,c){c||-1!=bf(e,a.history)||(Ce(a.history,b),e.push(a.history)),yc(a,b,null,zd(a,b))})}}function wc(a,b){if(!a.cm||!a.cm.state.suppressEdits){var c=a.history,d=("undo"==b?c.done:c.undone).pop();if(d){var e={changes:[],anchorBefore:d.anchorAfter,headBefore:d.headAfter,anchorAfter:d.anchorBefore,headAfter:d.headBefore,generation:c.generation};("undo"==b?c.undone:c.done).push(e),c.generation=d.generation||++c.maxGeneration;for(var f=Te(a,"beforeChange")||a.cm&&Te(a.cm,"beforeChange"),g=d.changes.length-1;g>=0;--g){var h=d.changes[g];if(h.origin=b,f&&!tc(a,h,!1))return("undo"==b?c.done:c.undone).length=0,void 0;e.changes.push(ve(a,h));var i=g?sc(a,h,null):{anchor:d.anchorBefore,head:d.headBefore};yc(a,h,i,Ad(a,h));var j=[];je(a,function(a,b){b||-1!=bf(j,a.history)||(Ce(a.history,h),j.push(a.history)),yc(a,h,null,Ad(a,h))})}}}}function xc(a,b){function c(a){return Bc(a.line+b,a.ch)}a.first+=b,a.cm&&Ib(a.cm,a.first,a.first,b),a.sel.head=c(a.sel.head),a.sel.anchor=c(a.sel.anchor),a.sel.from=c(a.sel.from),a.sel.to=c(a.sel.to)}function yc(a,b,c,d){if(a.cm&&!a.cm.curOp)return Fb(a.cm,yc)(a,b,c,d);if(b.to.line<a.first)return xc(a,b.text.length-1-(b.to.line-b.from.line)),void 0;if(!(b.from.line>a.lastLine())){if(b.from.line<a.first){var e=b.text.length-1-(a.first-b.from.line);xc(a,e),b={from:Bc(a.first,0),to:Bc(b.to.line+e,b.to.ch),text:[_e(b.text)],origin:b.origin}}var f=a.lastLine();b.to.line>f&&(b={from:b.from,to:Bc(f,le(a,f).text.length),text:[b.text[0]],origin:b.origin}),b.removed=me(a,b.from,b.to),c||(c=sc(a,b,null)),a.cm?zc(a.cm,b,d,c):ce(a,b,d,c)}}function zc(a,b,c,d){var e=a.doc,f=a.display,g=b.from,h=b.to,i=!1,j=g.line;a.options.lineWrapping||(j=pe(Fd(e,le(e,g.line))),e.iter(j,h.line+1,function(a){return a==f.maxLine?(i=!0,!0):void 0})),Dc(e.sel.head,b.from)||Dc(b.to,e.sel.head)||(a.curOp.cursorActivity=!0),ce(e,b,c,d,B(a)),a.options.lineWrapping||(e.iter(j,g.line+b.text.length,function(a){var b=H(e,a);b>f.maxLineLength&&(f.maxLine=a,f.maxLineLength=b,f.maxLineChanged=!0,i=!1)}),i&&(a.curOp.updateMaxLine=!0)),e.frontier=Math.min(e.frontier,g.line),bb(a,400);var k=b.text.length-(h.line-g.line)-1;if(Ib(a,g.line,h.line+1,k),Te(a,"change")){var l={from:g,to:h,text:b.text,removed:b.removed,origin:b.origin};if(a.curOp.textChanged){for(var m=a.curOp.textChanged;m.next;m=m.next);m.next=l}else a.curOp.textChanged=l}}function Ac(a,b,c,d,e){if(d||(d=c),Dc(d,c)){var f=d;d=c,c=f}"string"==typeof b&&(b=wf(b)),uc(a,{from:c,to:d,text:b,origin:e},null)}function Bc(a,b){return this instanceof Bc?(this.line=a,this.ch=b,void 0):new Bc(a,b)}function Cc(a,b){return a.line==b.line&&a.ch==b.ch}function Dc(a,b){return a.line<b.line||a.line==b.line&&a.ch<b.ch}function Ec(a){return Bc(a.line,a.ch)}function Fc(a,b){return Math.max(a.first,Math.min(b,a.first+a.size-1))}function Gc(a,b){if(b.line<a.first)return Bc(a.first,0);var c=a.first+a.size-1;return b.line>c?Bc(c,le(a,c).text.length):Hc(b,le(a,b.line).text.length)}function Hc(a,b){var c=a.ch;return null==c||c>b?Bc(a.line,b):0>c?Bc(a.line,0):a}function Ic(a,b){return b>=a.first&&b<a.first+a.size}function Jc(a,b,c,d){if(a.sel.shift||a.sel.extend){var e=a.sel.anchor;if(c){var f=Dc(b,e);f!=Dc(c,e)?(e=b,b=c):f!=Dc(b,c)&&(b=c)}Lc(a,e,b,d)}else Lc(a,b,c||b,d);a.cm&&(a.cm.curOp.userSelChange=!0)}function Kc(a,b,c){var d={anchor:b,head:c};return Ne(a,"beforeSelectionChange",a,d),a.cm&&Ne(a.cm,"beforeSelectionChange",a.cm,d),d.anchor=Gc(a,d.anchor),d.head=Gc(a,d.head),d}function Lc(a,b,c,d,e){if(!e&&Te(a,"beforeSelectionChange")||a.cm&&Te(a.cm,"beforeSelectionChange")){var f=Kc(a,b,c);c=f.head,b=f.anchor}var g=a.sel;if(g.goalColumn=null,null==d&&(d=Dc(c,g.head)?-1:1),(e||!Cc(b,g.anchor))&&(b=Nc(a,b,d,"push"!=e)),(e||!Cc(c,g.head))&&(c=Nc(a,c,d,"push"!=e)),!Cc(g.anchor,b)||!Cc(g.head,c)){g.anchor=b,g.head=c;var h=Dc(c,b);g.from=h?c:b,g.to=h?b:c,a.cm&&(a.cm.curOp.updateInput=a.cm.curOp.selectionChanged=a.cm.curOp.cursorActivity=!0),Qe(a,"cursorActivity",a)}}function Mc(a){Lc(a.doc,a.doc.sel.from,a.doc.sel.to,null,"push")}function Nc(a,b,c,d){var e=!1,f=b,g=c||1;a.cantEdit=!1;a:for(;;){var h=le(a,f.line);if(h.markedSpans)for(var i=0;i<h.markedSpans.length;++i){var j=h.markedSpans[i],k=j.marker;if((null==j.from||(k.inclusiveLeft?j.from<=f.ch:j.from<f.ch))&&(null==j.to||(k.inclusiveRight?j.to>=f.ch:j.to>f.ch))){if(d&&(Ne(k,"beforeCursorEnter"),k.explicitlyCleared)){if(h.markedSpans){--i;continue}break}if(!k.atomic)continue;var l=k.find()[0>g?"from":"to"];if(Cc(l,f)&&(l.ch+=g,l.ch<0?l=l.line>a.first?Gc(a,Bc(l.line-1)):null:l.ch>h.text.length&&(l=l.line<a.first+a.size-1?Bc(l.line+1,0):null),!l)){if(e)return d?(a.cantEdit=!0,Bc(a.first,0)):Nc(a,b,c,!0);e=!0,l=b,g=-g}f=l;continue a}}return f}}function Oc(a){var b=Pc(a,a.doc.sel.head,null,a.options.cursorScrollMargin);if(a.state.focused){var c=a.display,d=pf(c.sizer),e=null;if(b.top+d.top<0?e=!0:b.bottom+d.top>(window.innerHeight||document.documentElement.clientHeight)&&(e=!1),null!=e&&!n){var f="none"==c.cursor.style.display;f&&(c.cursor.style.display="",c.cursor.style.left=b.left+"px",c.cursor.style.top=b.top-c.viewOffset+"px"),c.cursor.scrollIntoView(e),f&&(c.cursor.style.display="none")}}}function Pc(a,b,c,d){for(null==d&&(d=0);;){var e=!1,f=vb(a,b),g=c&&c!=b?vb(a,c):f,h=Rc(a,Math.min(f.left,g.left),Math.min(f.top,g.top)-d,Math.max(f.left,g.left),Math.max(f.bottom,g.bottom)+d),i=a.doc.scrollTop,j=a.doc.scrollLeft;if(null!=h.scrollTop&&(_b(a,h.scrollTop),Math.abs(a.doc.scrollTop-i)>1&&(e=!0)),null!=h.scrollLeft&&(ac(a,h.scrollLeft),Math.abs(a.doc.scrollLeft-j)>1&&(e=!0)),!e)return f}}function Qc(a,b,c,d,e){var f=Rc(a,b,c,d,e);null!=f.scrollTop&&_b(a,f.scrollTop),null!=f.scrollLeft&&ac(a,f.scrollLeft)}function Rc(a,b,c,d,e){var f=a.display,g=Ab(a.display);0>c&&(c=0);var h=f.scroller.clientHeight-Ve,i=f.scroller.scrollTop,j={},k=a.doc.height+gb(f),l=g>c,m=e>k-g;if(i>c)j.scrollTop=l?0:c;else if(e>i+h){var n=Math.min(c,(m?k:e)-h);n!=i&&(j.scrollTop=n)}var o=f.scroller.clientWidth-Ve,p=f.scroller.scrollLeft;b+=f.gutters.offsetWidth,d+=f.gutters.offsetWidth;var q=f.gutters.offsetWidth,r=q+10>b;return p+q>b||r?(r&&(b=0),j.scrollLeft=Math.max(0,b-10-q)):d>o+p-3&&(j.scrollLeft=d+10-o),j}function Sc(a,b,c){a.curOp.updateScrollPos={scrollLeft:null==b?a.doc.scrollLeft:b,scrollTop:null==c?a.doc.scrollTop:c}}function Tc(a,b,c){var d=a.curOp.updateScrollPos||(a.curOp.updateScrollPos={scrollLeft:a.doc.scrollLeft,scrollTop:a.doc.scrollTop}),e=a.display.scroller;d.scrollTop=Math.max(0,Math.min(e.scrollHeight-e.clientHeight,d.scrollTop+c)),d.scrollLeft=Math.max(0,Math.min(e.scrollWidth-e.clientWidth,d.scrollLeft+b))}function Uc(a,b,c,d){var e=a.doc;if(null==c&&(c="add"),"smart"==c)if(a.doc.mode.indent)var f=eb(a,b);else c="prev";var k,g=a.options.tabSize,h=le(e,b),i=Ye(h.text,null,g),j=h.text.match(/^\s*/)[0];if("smart"==c&&(k=a.doc.mode.indent(f,h.text.slice(j.length),h.text),k==We)){if(!d)return;c="prev"}"prev"==c?k=b>e.first?Ye(le(e,b-1).text,null,g):0:"add"==c?k=i+a.options.indentUnit:"subtract"==c?k=i-a.options.indentUnit:"number"==typeof c&&(k=i+c),k=Math.max(0,k);var l="",m=0;if(a.options.indentWithTabs)for(var n=Math.floor(k/g);n;--n)m+=g,l+=" ";k>m&&(l+=$e(k-m)),l!=j?Ac(a.doc,l,Bc(b,0),Bc(b,j.length),"+input"):e.sel.head.line==b&&e.sel.head.ch<j.length&&Lc(e,Bc(b,j.length),Bc(b,j.length),1),h.stateAfter=null}function Vc(a,b,c){var d=b,e=b,f=a.doc;return"number"==typeof b?e=le(f,Fc(f,b)):d=pe(b),null==d?null:c(e,d)?(Ib(a,d,d+1),e):null}function Wc(a,b,c,d,e){function k(){var b=f+c;return b<a.first||b>=a.first+a.size?j=!1:(f=b,i=le(a,b))}function l(a){var b=(e?Lf:Mf)(i,g,c,!0);if(null==b){if(a||!k())return j=!1;g=e?(0>c?Ef:Df)(i):0>c?i.text.length:0}else g=b;return!0}var f=b.line,g=b.ch,h=c,i=le(a,f),j=!0;if("char"==d)l();else if("column"==d)l(!0);else if("word"==d||"group"==d)for(var m=null,n="group"==d,o=!0;!(0>c)||l(!o);o=!1){var p=i.text.charAt(g)||"\n",q=hf(p)?"w":n?/\s/.test(p)?null:"p":null;if(m&&m!=q){0>c&&(c=1,l());break}if(q&&(m=q),c>0&&!l(!o))break}var r=Nc(a,Bc(f,g),h,!0);return j||(r.hitSide=!0),r}function Xc(a,b,c,d){var g,e=a.doc,f=b.left;if("page"==d){var h=Math.min(a.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight);g=b.top+c*(h-(0>c?1.5:.5)*Ab(a.display))}else"line"==d&&(g=c>0?b.bottom+3:b.top-3);for(;;){var i=xb(a,f,g);if(!i.outside)break;if(0>c?0>=g:g>=e.height){i.hitSide=!0;break}g+=5*c}return i}function Yc(a,b){var c=b.ch,d=b.ch;if(a){(b.xRel<0||d==a.length)&&c?--c:++d;for(var e=a.charAt(c),f=hf(e)?hf:/\s/.test(e)?function(a){return/\s/.test(a)}:function(a){return!/\s/.test(a)&&!hf(a)};c>0&&f(a.charAt(c-1));)--c;for(;d<a.length&&f(a.charAt(d));)++d}return{from:Bc(b.line,c),to:Bc(b.line,d)}}function Zc(a,b){Jc(a.doc,Bc(b,0),Gc(a.doc,Bc(b+1,0)))}function ad(a,b,c,d){x.defaults[a]=b,c&&($c[a]=d?function(a,b,d){d!=bd&&c(a,b,d)}:c)}function hd(a,b){if(b===!0)return b;if(a.copyState)return a.copyState(b);var c={};for(var d in b){var e=b[d];e instanceof Array&&(e=e.concat([])),c[d]=e}return c}function id(a,b,c){return a.startState?a.startState(b,c):!0}function ld(a){return"string"==typeof a?kd[a]:a}function md(a,b,c){function d(b){b=ld(b);var e=b[a];if(e===!1)return"stop";if(null!=e&&c(e))return!0;if(b.nofallthrough)return"stop";var f=b.fallthrough;if(null==f)return!1;if("[object Array]"!=Object.prototype.toString.call(f))return d(f);for(var g=0,h=f.length;h>g;++g){var i=d(f[g]);if(i)return i}return!1}for(var e=0;e<b.length;++e){var f=d(b[e]);if(f)return"stop"!=f}}function nd(a){var b=zf[a.keyCode];return"Ctrl"==b||"Alt"==b||"Shift"==b||"Mod"==b}function od(a,b){if(i&&34==a.keyCode&&a["char"])return!1;var c=zf[a.keyCode];return null==c||a.altGraphKey?!1:(a.altKey&&(c="Alt-"+c),(t?a.metaKey:a.ctrlKey)&&(c="Ctrl-"+c),(t?a.ctrlKey:a.metaKey)&&(c="Cmd-"+c),!b&&a.shiftKey&&(c="Shift-"+c),c)}function pd(a,b){this.pos=this.start=0,this.string=a,this.tabSize=b||8,this.lastColumnPos=this.lastColumnValue=0}function qd(a,b){this.lines=[],this.type=b,this.doc=a}function rd(a,b,c,d,e){if(d&&d.shared)return td(a,b,c,d,e);if(a.cm&&!a.cm.curOp)return Fb(a.cm,rd)(a,b,c,d,e);var f=new qd(a,e);if(Dc(c,b)||Cc(b,c)&&"range"==e&&(!d.inclusiveLeft||!d.inclusiveRight))return f;d&&df(d,f),f.replacedWith&&(f.collapsed=!0,f.replacedWith=lf("span",[f.replacedWith],"CodeMirror-widget"),d.handleMouseEvents||(f.replacedWith.ignoreEvents=!0)),f.collapsed&&(w=!0),f.addToHistory&&we(a,{from:b,to:c,origin:"markText"},{head:a.sel.head,anchor:a.sel.anchor},0/0);var i,j,l,g=b.line,h=0,k=a.cm;if(a.iter(g,c.line+1,function(d){k&&f.collapsed&&!k.options.lineWrapping&&Fd(a,d)==k.display.maxLine&&(l=!0);var e={from:null,to:null,marker:f};h+=d.text.length,g==b.line&&(e.from=b.ch,h-=b.ch),g==c.line&&(e.to=c.ch,h-=d.text.length-c.ch),f.collapsed&&(g==c.line&&(j=Cd(d,c.ch)),g==b.line?i=Cd(d,b.ch):oe(d,0)),wd(d,e),++g}),f.collapsed&&a.iter(b.line,c.line+1,function(b){Gd(a,b)&&oe(b,0)}),f.clearOnEnter&&Le(f,"beforeCursorEnter",function(){f.clear()}),f.readOnly&&(v=!0,(a.history.done.length||a.history.undone.length)&&a.clearHistory()),f.collapsed){if(i!=j)throw new Error("Inserting collapsed marker overlapping an existing one");f.size=h,f.atomic=!0}return k&&(l&&(k.curOp.updateMaxLine=!0),(f.className||f.title||f.startStyle||f.endStyle||f.collapsed)&&Ib(k,b.line,c.line+1),f.atomic&&Mc(k)),f}function sd(a,b){this.markers=a,this.primary=b;for(var c=0,d=this;c<a.length;++c)a[c].parent=this,Le(a[c],"clear",function(){d.clear()})}function td(a,b,c,d,e){d=df(d),d.shared=!1;var f=[rd(a,b,c,d,e)],g=f[0],h=d.replacedWith;return je(a,function(a){h&&(d.replacedWith=h.cloneNode(!0)),f.push(rd(a,Gc(a,b),Gc(a,c),d,e));for(var i=0;i<a.linked.length;++i)if(a.linked[i].isParent)return;g=_e(f)}),new sd(f,g)}function ud(a,b){if(a)for(var c=0;c<a.length;++c){var d=a[c];if(d.marker==b)return d}}function vd(a,b){for(var c,d=0;d<a.length;++d)a[d]!=b&&(c||(c=[])).push(a[d]);return c}function wd(a,b){a.markedSpans=a.markedSpans?a.markedSpans.concat([b]):[b],b.marker.attachLine(a)}function xd(a,b,c){if(a)for(var e,d=0;d<a.length;++d){var f=a[d],g=f.marker,h=null==f.from||(g.inclusiveLeft?f.from<=b:f.from<b);if(h||(g.inclusiveLeft&&g.inclusiveRight||"bookmark"==g.type)&&f.from==b&&(!c||!f.marker.insertLeft)){var i=null==f.to||(g.inclusiveRight?f.to>=b:f.to>b);(e||(e=[])).push({from:f.from,to:i?null:f.to,marker:g})}}return e}function yd(a,b,c){if(a)for(var e,d=0;d<a.length;++d){var f=a[d],g=f.marker,h=null==f.to||(g.inclusiveRight?f.to>=b:f.to>b);if(h||"bookmark"==g.type&&f.from==b&&(!c||f.marker.insertLeft)){var i=null==f.from||(g.inclusiveLeft?f.from<=b:f.from<b);(e||(e=[])).push({from:i?null:f.from-b,to:null==f.to?null:f.to-b,marker:g})}}return e}function zd(a,b){var c=Ic(a,b.from.line)&&le(a,b.from.line).markedSpans,d=Ic(a,b.to.line)&&le(a,b.to.line).markedSpans;if(!c&&!d)return null;var e=b.from.ch,f=b.to.ch,g=Cc(b.from,b.to),h=xd(c,e,g),i=yd(d,f,g),j=1==b.text.length,k=_e(b.text).length+(j?e:0);if(h)for(var l=0;l<h.length;++l){var m=h[l];if(null==m.to){var n=ud(i,m.marker);n?j&&(m.to=null==n.to?null:n.to+k):m.to=e}}if(i)for(var l=0;l<i.length;++l){var m=i[l];if(null!=m.to&&(m.to+=k),null==m.from){var n=ud(h,m.marker);n||(m.from=k,j&&(h||(h=[])).push(m))}else m.from+=k,j&&(h||(h=[])).push(m)}if(j&&h){for(var l=0;l<h.length;++l)null!=h[l].from&&h[l].from==h[l].to&&"bookmark"!=h[l].marker.type&&h.splice(l--,1);h.length||(h=null)}var o=[h];if(!j){var q,p=b.text.length-2;if(p>0&&h)for(var l=0;l<h.length;++l)null==h[l].to&&(q||(q=[])).push({from:null,to:null,marker:h[l].marker});for(var l=0;p>l;++l)o.push(q);o.push(i)}return o}function Ad(a,b){var c=ye(a,b),d=zd(a,b);if(!c)return d;if(!d)return c;for(var e=0;e<c.length;++e){var f=c[e],g=d[e];if(f&&g)a:for(var h=0;h<g.length;++h){for(var i=g[h],j=0;j<f.length;++j)if(f[j].marker==i.marker)continue a;f.push(i)}else g&&(c[e]=g)}return c}function Bd(a,b,c){var d=null;if(a.iter(b.line,c.line+1,function(a){if(a.markedSpans)for(var b=0;b<a.markedSpans.length;++b){var c=a.markedSpans[b].marker;!c.readOnly||d&&-1!=bf(d,c)||(d||(d=[])).push(c)}}),!d)return null;for(var e=[{from:b,to:c}],f=0;f<d.length;++f)for(var g=d[f],h=g.find(),i=0;i<e.length;++i){var j=e[i];if(!Dc(j.to,h.from)&&!Dc(h.to,j.from)){var k=[i,1];(Dc(j.from,h.from)||!g.inclusiveLeft&&Cc(j.from,h.from))&&k.push({from:j.from,to:h.from}),(Dc(h.to,j.to)||!g.inclusiveRight&&Cc(j.to,h.to))&&k.push({from:h.to,to:j.to}),e.splice.apply(e,k),i+=k.length-1}}return e}function Cd(a,b){var d,c=w&&a.markedSpans;if(c)for(var e,f=0;f<c.length;++f)e=c[f],e.marker.collapsed&&(null==e.from||e.from<b)&&(null==e.to||e.to>b)&&(!d||d.width<e.marker.width)&&(d=e.marker);return d}function Dd(a){return Cd(a,-1)}function Ed(a){return Cd(a,a.text.length+1)}function Fd(a,b){for(var c;c=Dd(b);)b=le(a,c.find().from.line);return b}function Gd(a,b){var c=w&&b.markedSpans;if(c)for(var d,e=0;e<c.length;++e)if(d=c[e],d.marker.collapsed){if(null==d.from)return!0;if(!d.marker.replacedWith&&0==d.from&&d.marker.inclusiveLeft&&Hd(a,b,d))return!0}}function Hd(a,b,c){if(null==c.to){var d=c.marker.find().to,e=le(a,d.line);return Hd(a,e,ud(e.markedSpans,c.marker))}if(c.marker.inclusiveRight&&c.to==b.text.length)return!0;for(var f,g=0;g<b.markedSpans.length;++g)if(f=b.markedSpans[g],f.marker.collapsed&&!f.marker.replacedWith&&f.from==c.to&&(f.marker.inclusiveLeft||c.marker.inclusiveRight)&&Hd(a,b,f))return!0}function Id(a){var b=a.markedSpans;if(b){for(var c=0;c<b.length;++c)b[c].marker.detachLine(a);a.markedSpans=null}}function Jd(a,b){if(b){for(var c=0;c<b.length;++c)b[c].marker.attachLine(a);a.markedSpans=b}}function Ld(a){return function(){var b=!this.cm.curOp;b&&Db(this.cm);try{var c=a.apply(this,arguments)}finally{b&&Eb(this.cm)}return c}}function Md(a){return null!=a.height?a.height:(a.node.parentNode&&1==a.node.parentNode.nodeType||nf(a.cm.display.measure,lf("div",[a.node],null,"position: relative")),a.height=a.node.offsetHeight)}function Nd(a,b,c,d){var e=new Kd(a,c,d);return e.noHScroll&&(a.display.alignWidgets=!0),Vc(a,b,function(b){var c=b.widgets||(b.widgets=[]);if(null==e.insertAt?c.push(e):c.splice(Math.min(c.length-1,Math.max(0,e.insertAt)),0,e),e.line=b,!Gd(a.doc,b)||e.showIfHidden){var d=re(a,b)<a.doc.scrollTop;oe(b,b.height+Md(e)),d&&Tc(a,0,e.height)}return!0}),e}function Pd(a,b,c,d){a.text=b,a.stateAfter&&(a.stateAfter=null),a.styles&&(a.styles=null),null!=a.order&&(a.order=null),Id(a),Jd(a,c);var e=d?d(a):1;e!=a.height&&oe(a,e)}function Qd(a){a.parent=null,Id(a)}function Rd(a,b,c,d,e){var f=c.flattenSpans;null==f&&(f=a.options.flattenSpans);var j,g=0,h=null,i=new pd(b,a.options.tabSize);for(""==b&&c.blankLine&&c.blankLine(d);!i.eol();)i.pos>a.options.maxHighlightLength?(f=!1,i.pos=b.length,j=null):j=c.token(i,d),f&&h==j||(g<i.start&&e(i.start,h),g=i.start,h=j),i.start=i.pos;for(;g<i.pos;){var k=Math.min(i.pos,g+5e4);e(k,h),g=k}}function Sd(a,b,c){var d=[a.state.modeGen];Rd(a,b.text,a.doc.mode,c,function(a,b){d.push(a,b)});for(var e=0;e<a.state.overlays.length;++e){var f=a.state.overlays[e],g=1,h=0;Rd(a,b.text,f.mode,!0,function(a,b){for(var c=g;a>h;){var e=d[g];e>a&&d.splice(g,1,a,d[g+1],e),g+=2,h=Math.min(a,e)}if(b)if(f.opaque)d.splice(c,g-c,a,b),g=c+2;else for(;g>c;c+=2){var i=d[c+1];d[c+1]=i?i+" "+b:b}})}return d}function Td(a,b){return b.styles&&b.styles[0]==a.state.modeGen||(b.styles=Sd(a,b,b.stateAfter=eb(a,pe(b)))),b.styles}function Ud(a,b,c){var d=a.doc.mode,e=new pd(b.text,a.options.tabSize);for(""==b.text&&d.blankLine&&d.blankLine(c);!e.eol()&&e.pos<=a.options.maxHighlightLength;)d.token(e,c),e.start=e.pos}function Wd(a,b){if(!a)return null;for(;;){var c=a.match(/(?:^|\s)line-(background-)?(\S+)/);if(!c)break;a=a.slice(0,c.index)+a.slice(c.index+c[0].length);var d=c[1]?"bgClass":"textClass";null==b[d]?b[d]=c[2]:new RegExp("(?:^|s)"+c[2]+"(?:$|s)").test(b[d])||(b[d]+=" "+c[2])}return Vd[a]||(Vd[a]="cm-"+a.replace(/ +/g," cm-"))}function Xd(a,c,d,g){for(var h,i=c,j=!0;h=Dd(i);)i=le(a.doc,h.find().from.line);var k={pre:lf("pre"),col:0,pos:0,measure:null,measuredSomething:!1,cm:a,copyWidgets:g};do{i.text&&(j=!1),k.measure=i==c&&d,k.pos=0,k.addToken=k.measure?$d:Zd,(b||f)&&a.getOption("lineWrapping")&&(k.addToken=_d(k.addToken));var l=be(i,k,Td(a,i));d&&i==c&&!k.measuredSomething&&(d[0]=k.pre.appendChild(vf(a.display.measure)),k.measuredSomething=!0),l&&(i=le(a.doc,l.to.line))}while(l);!d||k.measuredSomething||d[0]||(d[0]=k.pre.appendChild(j?lf("span","\xa0"):vf(a.display.measure))),k.pre.firstChild||Gd(a.doc,c)||k.pre.appendChild(document.createTextNode("\xa0"));var m;if(d&&(b||e)&&(m=se(i))){var n=m.length-1;m[n].from==m[n].to&&--n;var o=m[n],p=m[n-1];if(o.from+1==o.to&&p&&o.level<p.level){var q=d[k.pos-1];q&&q.parentNode.insertBefore(q.measureRight=vf(a.display.measure),q.nextSibling)}}var r=k.textClass?k.textClass+" "+(c.textClass||""):c.textClass;return r&&(k.pre.className=r),Ne(a,"renderLine",a,c,k.pre),k}function Zd(a,b,c,d,e,f){if(b){if(Yd.test(b))for(var g=document.createDocumentFragment(),h=0;;){Yd.lastIndex=h;var i=Yd.exec(b),j=i?i.index-h:b.length-h;if(j&&(g.appendChild(document.createTextNode(b.slice(h,h+j))),a.col+=j),!i)break;if(h+=j+1," "==i[0]){var k=a.cm.options.tabSize,l=k-a.col%k;g.appendChild(lf("span",$e(l),"cm-tab")),a.col+=l}else{var m=lf("span","\u2022","cm-invalidchar");m.title="\\u"+i[0].charCodeAt(0).toString(16),g.appendChild(m),a.col+=1}}else{a.col+=b.length;var g=document.createTextNode(b)}if(c||d||e||a.measure){var n=c||"";d&&(n+=d),e&&(n+=e);var m=lf("span",[g],n);return f&&(m.title=f),a.pre.appendChild(m)}a.pre.appendChild(g)}}function $d(a,c,d,e,f){for(var g=a.cm.options.lineWrapping,h=0;h<c.length;++h){var i=c.charAt(h),j=0==h;i>="\ud800"&&"\udbff">i&&h<c.length-1?(i=c.slice(h,h+2),++h):h&&g&&rf(c,h)&&a.pre.appendChild(lf("wbr"));var k=a.measure[a.pos],l=a.measure[a.pos]=Zd(a,i,d,j&&e,h==c.length-1&&f);k&&(l.leftSide=k.leftSide||k),b&&g&&" "==i&&h&&!/\s/.test(c.charAt(h-1))&&h<c.length-1&&!/\s/.test(c.charAt(h+1))&&(l.style.whiteSpace="normal"),a.pos+=i.length}c.length&&(a.measuredSomething=!0)}function _d(a){function b(a){for(var b=" ",c=0;c<a.length-2;++c)b+=c%2?" ":"\xa0";return b+=" "}return function(c,d,e,f,g,h){return a(c,d.replace(/ {3,}/g,b),e,f,g,h)}}function ae(a,b,c,d){var e=!d&&c.replacedWith;if(e&&(a.copyWidgets&&(e=e.cloneNode(!0)),a.pre.appendChild(e),a.measure)){if(b)a.measure[a.pos]=e;else{var f=vf(a.cm.display.measure);if("bookmark"!=c.type||c.insertLeft){if(a.measure[a.pos])return;a.measure[a.pos]=a.pre.insertBefore(f,e)}else a.measure[a.pos]=a.pre.appendChild(f)}a.measuredSomething=!0}a.pos+=b}function be(a,b,c){var d=a.markedSpans,e=a.text,f=0;if(d)for(var k,m,n,o,p,q,h=e.length,i=0,g=1,j="",l=0;;){if(l==i){m=n=o=p="",q=null,l=1/0;for(var r=[],s=0;s<d.length;++s){var t=d[s],u=t.marker;t.from<=i&&(null==t.to||t.to>i)?(null!=t.to&&l>t.to&&(l=t.to,n=""),u.className&&(m+=" "+u.className),u.startStyle&&t.from==i&&(o+=" "+u.startStyle),u.endStyle&&t.to==l&&(n+=" "+u.endStyle),u.title&&!p&&(p=u.title),u.collapsed&&(!q||q.marker.size<u.size)&&(q=t)):t.from>i&&l>t.from&&(l=t.from),"bookmark"==u.type&&t.from==i&&u.replacedWith&&r.push(u)}if(q&&(q.from||0)==i&&(ae(b,(null==q.to?h:q.to)-i,q.marker,null==q.from),null==q.to))return q.marker.find();if(!q&&r.length)for(var s=0;s<r.length;++s)ae(b,0,r[s])
+}if(i>=h)break;for(var v=Math.min(h,l);;){if(j){var w=i+j.length;if(!q){var x=w>v?j.slice(0,v-i):j;b.addToken(b,x,k?k+m:m,o,i+x.length==l?n:"",p)}if(w>=v){j=j.slice(v-i),i=v;break}i=w,o=""}j=e.slice(f,f=c[g++]),k=Wd(c[g++],b)}}else for(var g=1;g<c.length;g+=2)b.addToken(b,e.slice(f,f=c[g]),Wd(c[g+1],b))}function ce(a,b,c,d,e){function f(a){return c?c[a]:null}function g(a,c,d){Pd(a,c,d,e),Qe(a,"change",a,b)}var h=b.from,i=b.to,j=b.text,k=le(a,h.line),l=le(a,i.line),m=_e(j),n=f(j.length-1),o=i.line-h.line;if(0==h.ch&&0==i.ch&&""==m){for(var p=0,q=j.length-1,r=[];q>p;++p)r.push(new Od(j[p],f(p),e));g(l,l.text,n),o&&a.remove(h.line,o),r.length&&a.insert(h.line,r)}else if(k==l)if(1==j.length)g(k,k.text.slice(0,h.ch)+m+k.text.slice(i.ch),n);else{for(var r=[],p=1,q=j.length-1;q>p;++p)r.push(new Od(j[p],f(p),e));r.push(new Od(m+k.text.slice(i.ch),n,e)),g(k,k.text.slice(0,h.ch)+j[0],f(0)),a.insert(h.line+1,r)}else if(1==j.length)g(k,k.text.slice(0,h.ch)+j[0]+l.text.slice(i.ch),f(0)),a.remove(h.line+1,o);else{g(k,k.text.slice(0,h.ch)+j[0],f(0)),g(l,m+l.text.slice(i.ch),n);for(var p=1,q=j.length-1,r=[];q>p;++p)r.push(new Od(j[p],f(p),e));o>1&&a.remove(h.line+1,o-1),a.insert(h.line+1,r)}Qe(a,"change",a,b),Lc(a,d.anchor,d.head,null,!0)}function de(a){this.lines=a,this.parent=null;for(var b=0,c=a.length,d=0;c>b;++b)a[b].parent=this,d+=a[b].height;this.height=d}function ee(a){this.children=a;for(var b=0,c=0,d=0,e=a.length;e>d;++d){var f=a[d];b+=f.chunkSize(),c+=f.height,f.parent=this}this.size=b,this.height=c,this.parent=null}function je(a,b,c){function d(a,e,f){if(a.linked)for(var g=0;g<a.linked.length;++g){var h=a.linked[g];if(h.doc!=e){var i=f&&h.sharedHist;(!c||i)&&(b(h.doc,i),d(h.doc,a,i))}}}d(a,null,!0)}function ke(a,b){if(b.cm)throw new Error("This document is already in use.");a.doc=b,b.cm=a,C(a),z(a),a.options.lineWrapping||I(a),a.options.mode=b.modeOption,Ib(a)}function le(a,b){for(b-=a.first;!a.lines;)for(var c=0;;++c){var d=a.children[c],e=d.chunkSize();if(e>b){a=d;break}b-=e}return a.lines[b]}function me(a,b,c){var d=[],e=b.line;return a.iter(b.line,c.line+1,function(a){var f=a.text;e==c.line&&(f=f.slice(0,c.ch)),e==b.line&&(f=f.slice(b.ch)),d.push(f),++e}),d}function ne(a,b,c){var d=[];return a.iter(b,c,function(a){d.push(a.text)}),d}function oe(a,b){for(var c=b-a.height,d=a;d;d=d.parent)d.height+=c}function pe(a){if(null==a.parent)return null;for(var b=a.parent,c=bf(b.lines,a),d=b.parent;d;b=d,d=d.parent)for(var e=0;d.children[e]!=b;++e)c+=d.children[e].chunkSize();return c+b.first}function qe(a,b){var c=a.first;a:do{for(var d=0,e=a.children.length;e>d;++d){var f=a.children[d],g=f.height;if(g>b){a=f;continue a}b-=g,c+=f.chunkSize()}return c}while(!a.lines);for(var d=0,e=a.lines.length;e>d;++d){var h=a.lines[d],i=h.height;if(i>b)break;b-=i}return c+d}function re(a,b){b=Fd(a.doc,b);for(var c=0,d=b.parent,e=0;e<d.lines.length;++e){var f=d.lines[e];if(f==b)break;c+=f.height}for(var g=d.parent;g;d=g,g=d.parent)for(var e=0;e<g.children.length;++e){var h=g.children[e];if(h==d)break;c+=h.height}return c}function se(a){var b=a.order;return null==b&&(b=a.order=Nf(a.text)),b}function te(a){return{done:[],undone:[],undoDepth:1/0,lastTime:0,lastOp:null,lastOrigin:null,generation:a||1,maxGeneration:a||1}}function ue(a,b,c,d){var e=b["spans_"+a.id],f=0;a.iter(Math.max(a.first,c),Math.min(a.first+a.size,d),function(c){c.markedSpans&&((e||(e=b["spans_"+a.id]={}))[f]=c.markedSpans),++f})}function ve(a,b){var c={line:b.from.line,ch:b.from.ch},d={from:c,to:qc(b),text:me(a,b.from,b.to)};return ue(a,d,b.from.line,b.to.line+1),je(a,function(a){ue(a,d,b.from.line,b.to.line+1)},!0),d}function we(a,b,c,d){var e=a.history;e.undone.length=0;var f=+new Date,g=_e(e.done);if(g&&(e.lastOp==d||e.lastOrigin==b.origin&&b.origin&&("+"==b.origin.charAt(0)&&a.cm&&e.lastTime>f-a.cm.options.historyEventDelay||"*"==b.origin.charAt(0)))){var h=_e(g.changes);Cc(b.from,b.to)&&Cc(b.from,h.to)?h.to=qc(b):g.changes.push(ve(a,b)),g.anchorAfter=c.anchor,g.headAfter=c.head}else for(g={changes:[ve(a,b)],generation:e.generation,anchorBefore:a.sel.anchor,headBefore:a.sel.head,anchorAfter:c.anchor,headAfter:c.head},e.done.push(g),e.generation=++e.maxGeneration;e.done.length>e.undoDepth;)e.done.shift();e.lastTime=f,e.lastOp=d,e.lastOrigin=b.origin}function xe(a){if(!a)return null;for(var c,b=0;b<a.length;++b)a[b].marker.explicitlyCleared?c||(c=a.slice(0,b)):c&&c.push(a[b]);return c?c.length?c:null:a}function ye(a,b){var c=b["spans_"+a.id];if(!c)return null;for(var d=0,e=[];d<b.text.length;++d)e.push(xe(c[d]));return e}function ze(a,b){for(var c=0,d=[];c<a.length;++c){var e=a[c],f=e.changes,g=[];d.push({changes:g,anchorBefore:e.anchorBefore,headBefore:e.headBefore,anchorAfter:e.anchorAfter,headAfter:e.headAfter});for(var h=0;h<f.length;++h){var j,i=f[h];if(g.push({from:i.from,to:i.to,text:i.text}),b)for(var k in i)(j=k.match(/^spans_(\d+)$/))&&bf(b,Number(j[1]))>-1&&(_e(g)[k]=i[k],delete i[k])}}return d}function Ae(a,b,c,d){c<a.line?a.line+=d:b<a.line&&(a.line=b,a.ch=0)}function Be(a,b,c,d){for(var e=0;e<a.length;++e){for(var f=a[e],g=!0,h=0;h<f.changes.length;++h){var i=f.changes[h];if(f.copied||(i.from=Ec(i.from),i.to=Ec(i.to)),c<i.from.line)i.from.line+=d,i.to.line+=d;else if(b<=i.to.line){g=!1;break}}f.copied||(f.anchorBefore=Ec(f.anchorBefore),f.headBefore=Ec(f.headBefore),f.anchorAfter=Ec(f.anchorAfter),f.readAfter=Ec(f.headAfter),f.copied=!0),g?(Ae(f.anchorBefore),Ae(f.headBefore),Ae(f.anchorAfter),Ae(f.headAfter)):(a.splice(0,e+1),e=0)}}function Ce(a,b){var c=b.from.line,d=b.to.line,e=b.text.length-(d-c)-1;Be(a.done,c,d,e),Be(a.undone,c,d,e)}function De(){Ie(this)}function Ee(a){return a.stop||(a.stop=De),a}function Fe(a){a.preventDefault?a.preventDefault():a.returnValue=!1}function Ge(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0}function He(a){return null!=a.defaultPrevented?a.defaultPrevented:0==a.returnValue}function Ie(a){Fe(a),Ge(a)}function Je(a){return a.target||a.srcElement}function Ke(a){var b=a.which;return null==b&&(1&a.button?b=1:2&a.button?b=3:4&a.button&&(b=2)),q&&a.ctrlKey&&1==b&&(b=3),b}function Le(a,b,c){if(a.addEventListener)a.addEventListener(b,c,!1);else if(a.attachEvent)a.attachEvent("on"+b,c);else{var d=a._handlers||(a._handlers={}),e=d[b]||(d[b]=[]);e.push(c)}}function Me(a,b,c){if(a.removeEventListener)a.removeEventListener(b,c,!1);else if(a.detachEvent)a.detachEvent("on"+b,c);else{var d=a._handlers&&a._handlers[b];if(!d)return;for(var e=0;e<d.length;++e)if(d[e]==c){d.splice(e,1);break}}}function Ne(a,b){var c=a._handlers&&a._handlers[b];if(c)for(var d=Array.prototype.slice.call(arguments,2),e=0;e<c.length;++e)c[e].apply(null,d)}function Qe(a,b){function e(a){return function(){a.apply(null,d)}}var c=a._handlers&&a._handlers[b];if(c){var d=Array.prototype.slice.call(arguments,2);Oe||(++Pe,Oe=[],setTimeout(Se,0));for(var f=0;f<c.length;++f)Oe.push(e(c[f]))}}function Re(a,b,c){return Ne(a,c||b.type,a,b),He(b)||b.codemirrorIgnore}function Se(){--Pe;var a=Oe;Oe=null;for(var b=0;b<a.length;++b)a[b]()}function Te(a,b){var c=a._handlers&&a._handlers[b];return c&&c.length>0}function Ue(a){a.prototype.on=function(a,b){Le(this,a,b)},a.prototype.off=function(a,b){Me(this,a,b)}}function Xe(){this.id=null}function Ye(a,b,c,d,e){null==b&&(b=a.search(/[^\s\u00a0]/),-1==b&&(b=a.length));for(var f=d||0,g=e||0;b>f;++f)" "==a.charAt(f)?g+=c-g%c:++g;return g}function $e(a){for(;Ze.length<=a;)Ze.push(_e(Ze)+" ");return Ze[a]}function _e(a){return a[a.length-1]}function af(a){if(o)a.selectionStart=0,a.selectionEnd=a.value.length;else try{a.select()}catch(b){}}function bf(a,b){if(a.indexOf)return a.indexOf(b);for(var c=0,d=a.length;d>c;++c)if(a[c]==b)return c;return-1}function cf(a,b){function c(){}c.prototype=a;var d=new c;return b&&df(b,d),d}function df(a,b){b||(b={});for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}function ef(a){for(var b=[],c=0;a>c;++c)b.push(void 0);return b}function ff(a){var b=Array.prototype.slice.call(arguments,1);return function(){return a.apply(null,b)}}function hf(a){return/\w/.test(a)||a>"\x80"&&(a.toUpperCase()!=a.toLowerCase()||gf.test(a))}function jf(a){for(var b in a)if(a.hasOwnProperty(b)&&a[b])return!1;return!0}function lf(a,b,c,d){var e=document.createElement(a);if(c&&(e.className=c),d&&(e.style.cssText=d),"string"==typeof b)of(e,b);else if(b)for(var f=0;f<b.length;++f)e.appendChild(b[f]);return e}function mf(a){for(var b=a.childNodes.length;b>0;--b)a.removeChild(a.firstChild);return a}function nf(a,b){return mf(a).appendChild(b)}function of(a,b){d?(a.innerHTML="",a.appendChild(document.createTextNode(b))):a.textContent=b}function pf(a){return a.getBoundingClientRect()}function rf(){return!1}function tf(a){if(null!=sf)return sf;var b=lf("div",null,null,"width: 50px; height: 50px; overflow-x: scroll");return nf(a,b),b.offsetWidth&&(sf=b.offsetHeight-b.clientHeight),sf||0}function vf(a){if(null==uf){var b=lf("span","\u200b");nf(a,lf("span",[b,document.createTextNode("x")])),0!=a.firstChild.offsetHeight&&(uf=b.offsetWidth<=1&&b.offsetHeight>2&&!c)}return uf?lf("span","\u200b"):lf("span","\xa0",null,"display: inline-block; width: 1px; margin-right: -1px")}function Af(a,b,c,d){if(!a)return d(b,c,"ltr");for(var e=!1,f=0;f<a.length;++f){var g=a[f];(g.from<c&&g.to>b||b==c&&g.to==b)&&(d(Math.max(g.from,b),Math.min(g.to,c),1==g.level?"rtl":"ltr"),e=!0)}e||d(b,c,"ltr")}function Bf(a){return a.level%2?a.to:a.from}function Cf(a){return a.level%2?a.from:a.to}function Df(a){var b=se(a);return b?Bf(b[0]):0}function Ef(a){var b=se(a);return b?Cf(_e(b)):a.text.length}function Ff(a,b){var c=le(a.doc,b),d=Fd(a.doc,c);d!=c&&(b=pe(d));var e=se(d),f=e?e[0].level%2?Ef(d):Df(d):0;return Bc(b,f)}function Gf(a,b){for(var c,d;c=Ed(d=le(a.doc,b));)b=c.find().to.line;var e=se(d),f=e?e[0].level%2?Df(d):Ef(d):d.text.length;return Bc(b,f)}function Hf(a,b,c){var d=a[0].level;return b==d?!0:c==d?!1:c>b}function Jf(a,b){for(var d,c=0;c<a.length;++c){var e=a[c];if(e.from<b&&e.to>b)return If=null,c;if(e.from==b||e.to==b){if(null!=d)return Hf(a,e.level,a[d].level)?(If=d,c):(If=c,d);d=c}}return If=null,d}function Kf(a,b,c,d){if(!d)return b+c;do b+=c;while(b>0&&kf.test(a.text.charAt(b)));return b}function Lf(a,b,c,d){var e=se(a);if(!e)return Mf(a,b,c,d);for(var f=Jf(e,b),g=e[f],h=Kf(a,b,g.level%2?-c:c,d);;){if(h>g.from&&h<g.to)return h;if(h==g.from||h==g.to)return Jf(e,h)==f?h:(g=e[f+=c],c>0==g.level%2?g.to:g.from);if(g=e[f+=c],!g)return null;h=c>0==g.level%2?Kf(a,g.to,-1,d):Kf(a,g.from,1,d)}}function Mf(a,b,c,d){var e=b+c;if(d)for(;e>0&&kf.test(a.text.charAt(e));)e+=c;return 0>e||e>a.text.length?null:e}var a=/gecko\/\d/i.test(navigator.userAgent),b=/MSIE \d/.test(navigator.userAgent),c=b&&(null==document.documentMode||document.documentMode<8),d=b&&(null==document.documentMode||document.documentMode<9),e=/Trident\/([7-9]|\d{2,})\./,f=/WebKit\//.test(navigator.userAgent),g=f&&/Qt\/\d+\.\d+/.test(navigator.userAgent),h=/Chrome\//.test(navigator.userAgent),i=/Opera\//.test(navigator.userAgent),j=/Apple Computer/.test(navigator.vendor),k=/KHTML\//.test(navigator.userAgent),l=/Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent),m=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent),n=/PhantomJS/.test(navigator.userAgent),o=/AppleWebKit/.test(navigator.userAgent)&&/Mobile\/\w+/.test(navigator.userAgent),p=o||/Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent),q=o||/Mac/.test(navigator.platform),r=/win/i.test(navigator.platform),s=i&&navigator.userAgent.match(/Version\/(\d*\.\d*)/);s&&(s=Number(s[1])),s&&s>=15&&(i=!1,f=!0);var zb,Sb,Tb,t=q&&(g||i&&(null==s||12.11>s)),u=a||b&&!d,v=!1,w=!1,Cb=0,Yb=0,bc=0,cc=null;b?cc=-.53:a?cc=15:h?cc=-.7:j&&(cc=-1/3);var gc,oc,jc=null,qc=x.changeEnd=function(a){return a.text?Bc(a.from.line+a.text.length-1,_e(a.text).length+(1==a.text.length?a.from.ch:0)):a.to};x.Pos=Bc,x.prototype={constructor:x,focus:function(){window.focus(),Nb(this),mc(this),Kb(this)},setOption:function(a,b){var c=this.options,d=c[a];(c[a]!=b||"mode"==a)&&(c[a]=b,$c.hasOwnProperty(a)&&Fb(this,$c[a])(this,b,d))},getOption:function(a){return this.options[a]},getDoc:function(){return this.doc},addKeyMap:function(a,b){this.state.keyMaps[b?"push":"unshift"](a)},removeKeyMap:function(a){for(var b=this.state.keyMaps,c=0;c<b.length;++c)if(b[c]==a||"string"!=typeof b[c]&&b[c].name==a)return b.splice(c,1),!0},addOverlay:Fb(null,function(a,b){var c=a.token?a:x.getMode(this.options,a);if(c.startState)throw new Error("Overlays may not be stateful.");this.state.overlays.push({mode:c,modeSpec:a,opaque:b&&b.opaque}),this.state.modeGen++,Ib(this)}),removeOverlay:Fb(null,function(a){for(var b=this.state.overlays,c=0;c<b.length;++c){var d=b[c].modeSpec;if(d==a||"string"==typeof a&&d.name==a)return b.splice(c,1),this.state.modeGen++,Ib(this),void 0}}),indentLine:Fb(null,function(a,b,c){"string"!=typeof b&&"number"!=typeof b&&(b=null==b?this.options.smartIndent?"smart":"prev":b?"add":"subtract"),Ic(this.doc,a)&&Uc(this,a,b,c)}),indentSelection:Fb(null,function(a){var b=this.doc.sel;if(Cc(b.from,b.to))return Uc(this,b.from.line,a);for(var c=b.to.line-(b.to.ch?0:1),d=b.from.line;c>=d;++d)Uc(this,d,a)}),getTokenAt:function(a,b){var c=this.doc;a=Gc(c,a);for(var d=eb(this,a.line,b),e=this.doc.mode,f=le(c,a.line),g=new pd(f.text,this.options.tabSize);g.pos<a.ch&&!g.eol();){g.start=g.pos;var h=e.token(g,d)}return{start:g.start,end:g.pos,string:g.current(),className:h||null,type:h||null,state:d}},getTokenTypeAt:function(a){a=Gc(this.doc,a);var b=Td(this,le(this.doc,a.line)),c=0,d=(b.length-1)/2,e=a.ch;if(0==e)return b[2];for(;;){var f=c+d>>1;if((f?b[2*f-1]:0)>=e)d=f;else{if(!(b[2*f+1]<e))return b[2*f+2];c=f+1}}},getModeAt:function(a){var b=this.doc.mode;return b.innerMode?x.innerMode(b,this.getTokenAt(a).state).mode:b},getHelper:function(a,b){if(gd.hasOwnProperty(b)){var c=gd[b],d=this.getModeAt(a);return d[b]&&c[d[b]]||d.helperType&&c[d.helperType]||c[d.name]}},getStateAfter:function(a,b){var c=this.doc;return a=Fc(c,null==a?c.first+c.size-1:a),eb(this,a+1,b)},cursorCoords:function(a,b){var c,d=this.doc.sel;return c=null==a?d.head:"object"==typeof a?Gc(this.doc,a):a?d.from:d.to,vb(this,c,b||"page")},charCoords:function(a,b){return ub(this,Gc(this.doc,a),b||"page")},coordsChar:function(a,b){return a=tb(this,a,b||"page"),xb(this,a.left,a.top)},lineAtHeight:function(a,b){return a=tb(this,{top:a,left:0},b||"page").top,qe(this.doc,a+this.display.viewOffset)},heightAtLine:function(a,b){var c=!1,d=this.doc.first+this.doc.size-1;a<this.doc.first?a=this.doc.first:a>d&&(a=d,c=!0);var e=le(this.doc,a);return sb(this,le(this.doc,a),{top:0,left:0},b||"page").top+(c?e.height:0)},defaultTextHeight:function(){return Ab(this.display)},defaultCharWidth:function(){return Bb(this.display)},setGutterMarker:Fb(null,function(a,b,c){return Vc(this,a,function(a){var d=a.gutterMarkers||(a.gutterMarkers={});return d[b]=c,!c&&jf(d)&&(a.gutterMarkers=null),!0})}),clearGutter:Fb(null,function(a){var b=this,c=b.doc,d=c.first;c.iter(function(c){c.gutterMarkers&&c.gutterMarkers[a]&&(c.gutterMarkers[a]=null,Ib(b,d,d+1),jf(c.gutterMarkers)&&(c.gutterMarkers=null)),++d})}),addLineClass:Fb(null,function(a,b,c){return Vc(this,a,function(a){var d="text"==b?"textClass":"background"==b?"bgClass":"wrapClass";if(a[d]){if(new RegExp("(?:^|\\s)"+c+"(?:$|\\s)").test(a[d]))return!1;a[d]+=" "+c}else a[d]=c;return!0})}),removeLineClass:Fb(null,function(a,b,c){return Vc(this,a,function(a){var d="text"==b?"textClass":"background"==b?"bgClass":"wrapClass",e=a[d];if(!e)return!1;if(null==c)a[d]=null;else{var f=e.match(new RegExp("(?:^|\\s+)"+c+"(?:$|\\s+)"));if(!f)return!1;var g=f.index+f[0].length;a[d]=e.slice(0,f.index)+(f.index&&g!=e.length?" ":"")+e.slice(g)||null}return!0})}),addLineWidget:Fb(null,function(a,b,c){return Nd(this,a,b,c)}),removeLineWidget:function(a){a.clear()},lineInfo:function(a){if("number"==typeof a){if(!Ic(this.doc,a))return null;var b=a;if(a=le(this.doc,a),!a)return null}else{var b=pe(a);if(null==b)return null}return{line:b,handle:a,text:a.text,gutterMarkers:a.gutterMarkers,textClass:a.textClass,bgClass:a.bgClass,wrapClass:a.wrapClass,widgets:a.widgets}},getViewport:function(){return{from:this.display.showingFrom,to:this.display.showingTo}},addWidget:function(a,b,c,d,e){var f=this.display;a=vb(this,Gc(this.doc,a));var g=a.bottom,h=a.left;if(b.style.position="absolute",f.sizer.appendChild(b),"over"==d)g=a.top;else if("above"==d||"near"==d){var i=Math.max(f.wrapper.clientHeight,this.doc.height),j=Math.max(f.sizer.clientWidth,f.lineSpace.clientWidth);("above"==d||a.bottom+b.offsetHeight>i)&&a.top>b.offsetHeight?g=a.top-b.offsetHeight:a.bottom+b.offsetHeight<=i&&(g=a.bottom),h+b.offsetWidth>j&&(h=j-b.offsetWidth)}b.style.top=g+"px",b.style.left=b.style.right="","right"==e?(h=f.sizer.clientWidth-b.offsetWidth,b.style.right="0px"):("left"==e?h=0:"middle"==e&&(h=(f.sizer.clientWidth-b.offsetWidth)/2),b.style.left=h+"px"),c&&Qc(this,h,g,h+b.offsetWidth,g+b.offsetHeight)},triggerOnKeyDown:Fb(null,kc),execCommand:function(a){return jd[a](this)},findPosH:function(a,b,c,d){var e=1;0>b&&(e=-1,b=-b);for(var f=0,g=Gc(this.doc,a);b>f&&(g=Wc(this.doc,g,e,c,d),!g.hitSide);++f);return g},moveH:Fb(null,function(a,b){var d,c=this.doc.sel;d=c.shift||c.extend||Cc(c.from,c.to)?Wc(this.doc,c.head,a,b,this.options.rtlMoveVisually):0>a?c.from:c.to,Jc(this.doc,d,d,a)}),deleteH:Fb(null,function(a,b){var c=this.doc.sel;Cc(c.from,c.to)?Ac(this.doc,"",c.from,Wc(this.doc,c.head,a,b,!1),"+delete"):Ac(this.doc,"",c.from,c.to,"+delete"),this.curOp.userSelChange=!0}),findPosV:function(a,b,c,d){var e=1,f=d;0>b&&(e=-1,b=-b);for(var g=0,h=Gc(this.doc,a);b>g;++g){var i=vb(this,h,"div");if(null==f?f=i.left:i.left=f,h=Xc(this,i,e,c),h.hitSide)break}return h},moveV:Fb(null,function(a,b){var c=this.doc.sel,d=vb(this,c.head,"div");null!=c.goalColumn&&(d.left=c.goalColumn);var e=Xc(this,d,a,b);"page"==b&&Tc(this,0,ub(this,e,"div").top-d.top),Jc(this.doc,e,e,a),c.goalColumn=d.left}),toggleOverwrite:function(a){(null==a||a!=this.state.overwrite)&&((this.state.overwrite=!this.state.overwrite)?this.display.cursor.className+=" CodeMirror-overwrite":this.display.cursor.className=this.display.cursor.className.replace(" CodeMirror-overwrite",""))},hasFocus:function(){return this.state.focused},scrollTo:Fb(null,function(a,b){Sc(this,a,b)}),getScrollInfo:function(){var a=this.display.scroller,b=Ve;return{left:a.scrollLeft,top:a.scrollTop,height:a.scrollHeight-b,width:a.scrollWidth-b,clientHeight:a.clientHeight-b,clientWidth:a.clientWidth-b}},scrollIntoView:Fb(null,function(a,b){null==a?a={from:this.doc.sel.head,to:null}:"number"==typeof a?a={from:Bc(a,0),to:null}:null==a.from&&(a={from:a,to:null}),a.to||(a.to=a.from),b||(b=0);var c=a;null!=a.from.line&&(this.curOp.scrollToPos={from:a.from,to:a.to,margin:b},c={from:vb(this,a.from),to:vb(this,a.to)});var d=Rc(this,Math.min(c.from.left,c.to.left),Math.min(c.from.top,c.to.top)-b,Math.max(c.from.right,c.to.right),Math.max(c.from.bottom,c.to.bottom)+b);Sc(this,d.scrollLeft,d.scrollTop)}),setSize:Fb(null,function(a,b){function c(a){return"number"==typeof a||/^\d+$/.test(String(a))?a+"px":a}null!=a&&(this.display.wrapper.style.width=c(a)),null!=b&&(this.display.wrapper.style.height=c(b)),this.options.lineWrapping&&(this.display.measureLineCache.length=this.display.measureLineCachePos=0),this.curOp.forceUpdate=!0}),operation:function(a){return Hb(this,a)},refresh:Fb(null,function(){var a=null==this.display.cachedTextHeight;pb(this),Sc(this,this.doc.scrollLeft,this.doc.scrollTop),Ib(this),a&&C(this)}),swapDoc:Fb(null,function(a){var b=this.doc;return b.cm=null,ke(this,a),pb(this),Mb(this,!0),Sc(this,a.scrollLeft,a.scrollTop),Qe(this,"swapDoc",this,b),b}),getInputField:function(){return this.display.input},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Ue(x);var $c=x.optionHandlers={},_c=x.defaults={},bd=x.Init={toString:function(){return"CodeMirror.Init"}};ad("value","",function(a,b){a.setValue(b)},!0),ad("mode",null,function(a,b){a.doc.modeOption=b,z(a)},!0),ad("indentUnit",2,z,!0),ad("indentWithTabs",!1),ad("smartIndent",!0),ad("tabSize",4,function(a){z(a),pb(a),Ib(a)},!0),ad("electricChars",!0),ad("rtlMoveVisually",!r),ad("theme","default",function(a){E(a),F(a)},!0),ad("keyMap","default",D),ad("extraKeys",null),ad("onKeyEvent",null),ad("onDragEvent",null),ad("lineWrapping",!1,A,!0),ad("gutters",[],function(a){J(a.options),F(a)},!0),ad("fixedGutter",!0,function(a,b){a.display.gutters.style.left=b?P(a.display)+"px":"0",a.refresh()},!0),ad("coverGutterNextToScrollbar",!1,K,!0),ad("lineNumbers",!1,function(a){J(a.options),F(a)},!0),ad("firstLineNumber",1,F,!0),ad("lineNumberFormatter",function(a){return a},F,!0),ad("showCursorWhenSelecting",!1,Z,!0),ad("resetSelectionOnContextMenu",!0),ad("readOnly",!1,function(a,b){"nocursor"==b?(nc(a),a.display.input.blur(),a.display.disabled=!0):(a.display.disabled=!1,b||Mb(a,!0))}),ad("dragDrop",!0),ad("cursorBlinkRate",530),ad("cursorScrollMargin",0),ad("cursorHeight",1),ad("workTime",100),ad("workDelay",100),ad("flattenSpans",!0),ad("pollInterval",100),ad("undoDepth",40,function(a,b){a.doc.history.undoDepth=b}),ad("historyEventDelay",500),ad("viewportMargin",10,function(a){a.refresh()},!0),ad("maxHighlightLength",1e4,function(a){z(a),a.refresh()},!0),ad("crudeMeasuringFrom",1e4),ad("moveInputWithCursor",!0,function(a,b){b||(a.display.inputDiv.style.top=a.display.inputDiv.style.left=0)}),ad("tabindex",null,function(a,b){a.display.input.tabIndex=b||""}),ad("autofocus",null);var cd=x.modes={},dd=x.mimeModes={};x.defineMode=function(a,b){if(x.defaults.mode||"null"==a||(x.defaults.mode=a),arguments.length>2){b.dependencies=[];for(var c=2;c<arguments.length;++c)b.dependencies.push(arguments[c])}cd[a]=b},x.defineMIME=function(a,b){dd[a]=b},x.resolveMode=function(a){if("string"==typeof a&&dd.hasOwnProperty(a))a=dd[a];else if(a&&"string"==typeof a.name&&dd.hasOwnProperty(a.name)){var b=dd[a.name];a=cf(b,a),a.name=b.name}else if("string"==typeof a&&/^[\w\-]+\/[\w\-]+\+xml$/.test(a))return x.resolveMode("application/xml");return"string"==typeof a?{name:a}:a||{name:"null"}},x.getMode=function(a,b){var b=x.resolveMode(b),c=cd[b.name];if(!c)return x.getMode(a,"text/plain");var d=c(a,b);if(ed.hasOwnProperty(b.name)){var e=ed[b.name];for(var f in e)e.hasOwnProperty(f)&&(d.hasOwnProperty(f)&&(d["_"+f]=d[f]),d[f]=e[f])}return d.name=b.name,d},x.defineMode("null",function(){return{token:function(a){a.skipToEnd()}}}),x.defineMIME("text/plain","null");var ed=x.modeExtensions={};x.extendMode=function(a,b){var c=ed.hasOwnProperty(a)?ed[a]:ed[a]={};df(b,c)},x.defineExtension=function(a,b){x.prototype[a]=b},x.defineDocExtension=function(a,b){ge.prototype[a]=b},x.defineOption=ad;var fd=[];x.defineInitHook=function(a){fd.push(a)};var gd=x.helpers={};x.registerHelper=function(a,b,c){gd.hasOwnProperty(a)||(gd[a]=x[a]={}),gd[a][b]=c},x.isWordChar=hf,x.copyState=hd,x.startState=id,x.innerMode=function(a,b){for(;a.innerMode;){var c=a.innerMode(b);if(!c||c.mode==a)break;b=c.state,a=c.mode}return c||{mode:a,state:b}};var jd=x.commands={selectAll:function(a){a.setSelection(Bc(a.firstLine(),0),Bc(a.lastLine()))},killLine:function(a){var b=a.getCursor(!0),c=a.getCursor(!1),d=!Cc(b,c);d||a.getLine(b.line).length!=b.ch?a.replaceRange("",b,d?c:Bc(b.line),"+delete"):a.replaceRange("",b,Bc(b.line+1,0),"+delete")},deleteLine:function(a){var b=a.getCursor().line;a.replaceRange("",Bc(b,0),Bc(b),"+delete")},delLineLeft:function(a){var b=a.getCursor();a.replaceRange("",Bc(b.line,0),b,"+delete")},undo:function(a){a.undo()},redo:function(a){a.redo()},goDocStart:function(a){a.extendSelection(Bc(a.firstLine(),0))},goDocEnd:function(a){a.extendSelection(Bc(a.lastLine()))},goLineStart:function(a){a.extendSelection(Ff(a,a.getCursor().line))},goLineStartSmart:function(a){var b=a.getCursor(),c=Ff(a,b.line),d=a.getLineHandle(c.line),e=se(d);if(e&&0!=e[0].level)a.extendSelection(c);else{var f=Math.max(0,d.text.search(/\S/)),g=b.line==c.line&&b.ch<=f&&b.ch;a.extendSelection(Bc(c.line,g?0:f))}},goLineEnd:function(a){a.extendSelection(Gf(a,a.getCursor().line))},goLineRight:function(a){var b=a.charCoords(a.getCursor(),"div").top+5;a.extendSelection(a.coordsChar({left:a.display.lineDiv.offsetWidth+100,top:b},"div"))},goLineLeft:function(a){var b=a.charCoords(a.getCursor(),"div").top+5;a.extendSelection(a.coordsChar({left:0,top:b},"div"))},goLineUp:function(a){a.moveV(-1,"line")},goLineDown:function(a){a.moveV(1,"line")},goPageUp:function(a){a.moveV(-1,"page")},goPageDown:function(a){a.moveV(1,"page")},goCharLeft:function(a){a.moveH(-1,"char")},goCharRight:function(a){a.moveH(1,"char")},goColumnLeft:function(a){a.moveH(-1,"column")},goColumnRight:function(a){a.moveH(1,"column")},goWordLeft:function(a){a.moveH(-1,"word")},goGroupRight:function(a){a.moveH(1,"group")},goGroupLeft:function(a){a.moveH(-1,"group")},goWordRight:function(a){a.moveH(1,"word")},delCharBefore:function(a){a.deleteH(-1,"char")},delCharAfter:function(a){a.deleteH(1,"char")},delWordBefore:function(a){a.deleteH(-1,"word")},delWordAfter:function(a){a.deleteH(1,"word")},delGroupBefore:function(a){a.deleteH(-1,"group")},delGroupAfter:function(a){a.deleteH(1,"group")},indentAuto:function(a){a.indentSelection("smart")},indentMore:function(a){a.indentSelection("add")},indentLess:function(a){a.indentSelection("subtract")},insertTab:function(a){a.replaceSelection(" ","end","+input")},defaultTab:function(a){a.somethingSelected()?a.indentSelection("add"):a.replaceSelection(" ","end","+input")},transposeChars:function(a){var b=a.getCursor(),c=a.getLine(b.line);b.ch>0&&b.ch<c.length-1&&a.replaceRange(c.charAt(b.ch)+c.charAt(b.ch-1),Bc(b.line,b.ch-1),Bc(b.line,b.ch+1))},newlineAndIndent:function(a){Fb(a,function(){a.replaceSelection("\n","end","+input"),a.indentLine(a.getCursor().line,null,!0)})()},toggleOverwrite:function(a){a.toggleOverwrite()}},kd=x.keyMap={};kd.basic={Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore","Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite"},kd.pcDefault={"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart","Alt-Up":"goDocStart","Ctrl-End":"goDocEnd","Ctrl-Down":"goDocEnd","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter","Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore",fallthrough:"basic"},kd.macDefault={"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineStart","Cmd-Right":"goLineEnd","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter","Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delLineLeft",fallthrough:["basic","emacsy"]},kd["default"]=q?kd.macDefault:kd.pcDefault,kd.emacsy={"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Alt-F":"goWordRight","Alt-B":"goWordLeft","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp","Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-D":"delWordAfter","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars"},x.lookupKey=md,x.isModifierKey=nd,x.keyName=od,x.fromTextArea=function(a,b){function e(){a.value=i.getValue()}if(b||(b={}),b.value=a.value,!b.tabindex&&a.tabindex&&(b.tabindex=a.tabindex),!b.placeholder&&a.placeholder&&(b.placeholder=a.placeholder),null==b.autofocus){var c=document.body;try{c=document.activeElement}catch(d){}b.autofocus=c==a||null!=a.getAttribute("autofocus")&&c==document.body}if(a.form&&(Le(a.form,"submit",e),!b.leaveSubmitMethodAlone)){var f=a.form,g=f.submit;try{var h=f.submit=function(){e(),f.submit=g,f.submit(),f.submit=h}}catch(d){}}a.style.display="none";var i=x(function(b){a.parentNode.insertBefore(b,a.nextSibling)},b);return i.save=e,i.getTextArea=function(){return a},i.toTextArea=function(){e(),a.parentNode.removeChild(i.getWrapperElement()),a.style.display="",a.form&&(Me(a.form,"submit",e),"function"==typeof a.form.submit&&(a.form.submit=g))},i},pd.prototype={eol:function(){return this.pos>=this.string.length},sol:function(){return 0==this.pos},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){return this.pos<this.string.length?this.string.charAt(this.pos++):void 0},eat:function(a){var b=this.string.charAt(this.pos);if("string"==typeof a)var c=b==a;else var c=b&&(a.test?a.test(b):a(b));return c?(++this.pos,b):void 0},eatWhile:function(a){for(var b=this.pos;this.eat(a););return this.pos>b},eatSpace:function(){for(var a=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>a},skipToEnd:function(){this.pos=this.string.length},skipTo:function(a){var b=this.string.indexOf(a,this.pos);return b>-1?(this.pos=b,!0):void 0},backUp:function(a){this.pos-=a},column:function(){return this.lastColumnPos<this.start&&(this.lastColumnValue=Ye(this.string,this.start,this.tabSize,this.lastColumnPos,this.lastColumnValue),this.lastColumnPos=this.start),this.lastColumnValue},indentation:function(){return Ye(this.string,null,this.tabSize)},match:function(a,b,c){if("string"!=typeof a){var f=this.string.slice(this.pos).match(a);return f&&f.index>0?null:(f&&b!==!1&&(this.pos+=f[0].length),f)}var d=function(a){return c?a.toLowerCase():a},e=this.string.substr(this.pos,a.length);return d(e)==d(a)?(b!==!1&&(this.pos+=a.length),!0):void 0},current:function(){return this.string.slice(this.start,this.pos)}},x.StringStream=pd,x.TextMarker=qd,Ue(qd),qd.prototype.clear=function(){if(!this.explicitlyCleared){var a=this.doc.cm,b=a&&!a.curOp;if(b&&Db(a),Te(this,"clear")){var c=this.find();c&&Qe(this,"clear",c.from,c.to)}for(var d=null,e=null,f=0;f<this.lines.length;++f){var g=this.lines[f],h=ud(g.markedSpans,this);null!=h.to&&(e=pe(g)),g.markedSpans=vd(g.markedSpans,h),null!=h.from?d=pe(g):this.collapsed&&!Gd(this.doc,g)&&a&&oe(g,Ab(a.display))}if(a&&this.collapsed&&!a.options.lineWrapping)for(var f=0;f<this.lines.length;++f){var i=Fd(a.doc,this.lines[f]),j=H(a.doc,i);j>a.display.maxLineLength&&(a.display.maxLine=i,a.display.maxLineLength=j,a.display.maxLineChanged=!0)}null!=d&&a&&Ib(a,d,e+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,a&&Mc(a)),b&&Eb(a)}},qd.prototype.find=function(){for(var a,b,c=0;c<this.lines.length;++c){var d=this.lines[c],e=ud(d.markedSpans,this);if(null!=e.from||null!=e.to){var f=pe(d);null!=e.from&&(a=Bc(f,e.from)),null!=e.to&&(b=Bc(f,e.to))}}return"bookmark"==this.type?a:a&&{from:a,to:b}},qd.prototype.changed=function(){var a=this.find(),b=this.doc.cm;if(a&&b){"bookmark"!=this.type&&(a=a.from);var c=le(this.doc,a.line);if(kb(b,c),a.line>=b.display.showingFrom&&a.line<b.display.showingTo){for(var d=b.display.lineDiv.firstChild;d;d=d.nextSibling)if(d.lineObj==c){d.offsetHeight!=c.height&&oe(c,d.offsetHeight);break}Hb(b,function(){b.curOp.selectionChanged=b.curOp.forceUpdate=b.curOp.updateMaxLine=!0})}}},qd.prototype.attachLine=function(a){if(!this.lines.length&&this.doc.cm){var b=this.doc.cm.curOp;b.maybeHiddenMarkers&&-1!=bf(b.maybeHiddenMarkers,this)||(b.maybeUnhiddenMarkers||(b.maybeUnhiddenMarkers=[])).push(this)}this.lines.push(a)},qd.prototype.detachLine=function(a){if(this.lines.splice(bf(this.lines,a),1),!this.lines.length&&this.doc.cm){var b=this.doc.cm.curOp;
+(b.maybeHiddenMarkers||(b.maybeHiddenMarkers=[])).push(this)}},x.SharedTextMarker=sd,Ue(sd),sd.prototype.clear=function(){if(!this.explicitlyCleared){this.explicitlyCleared=!0;for(var a=0;a<this.markers.length;++a)this.markers[a].clear();Qe(this,"clear")}},sd.prototype.find=function(){return this.primary.find()};var Kd=x.LineWidget=function(a,b,c){if(c)for(var d in c)c.hasOwnProperty(d)&&(this[d]=c[d]);this.cm=a,this.node=b};Ue(Kd),Kd.prototype.clear=Ld(function(){var a=this.line.widgets,b=pe(this.line);if(null!=b&&a){for(var c=0;c<a.length;++c)a[c]==this&&a.splice(c--,1);a.length||(this.line.widgets=null);var d=re(this.cm,this.line)<this.cm.doc.scrollTop;oe(this.line,Math.max(0,this.line.height-Md(this))),d&&Tc(this.cm,0,-this.height),Ib(this.cm,b,b+1)}}),Kd.prototype.changed=Ld(function(){var a=this.height;this.height=null;var b=Md(this)-a;if(b){oe(this.line,this.line.height+b);var c=pe(this.line);Ib(this.cm,c,c+1)}});var Od=x.Line=function(a,b,c){this.text=a,Jd(this,b),this.height=c?c(this):1};Ue(Od);var Vd={},Yd=/[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\uFEFF]/g;de.prototype={chunkSize:function(){return this.lines.length},removeInner:function(a,b){for(var c=a,d=a+b;d>c;++c){var e=this.lines[c];this.height-=e.height,Qd(e),Qe(e,"delete")}this.lines.splice(a,b)},collapse:function(a){a.splice.apply(a,[a.length,0].concat(this.lines))},insertInner:function(a,b,c){this.height+=c,this.lines=this.lines.slice(0,a).concat(b).concat(this.lines.slice(a));for(var d=0,e=b.length;e>d;++d)b[d].parent=this},iterN:function(a,b,c){for(var d=a+b;d>a;++a)if(c(this.lines[a]))return!0}},ee.prototype={chunkSize:function(){return this.size},removeInner:function(a,b){this.size-=b;for(var c=0;c<this.children.length;++c){var d=this.children[c],e=d.chunkSize();if(e>a){var f=Math.min(b,e-a),g=d.height;if(d.removeInner(a,f),this.height-=g-d.height,e==f&&(this.children.splice(c--,1),d.parent=null),0==(b-=f))break;a=0}else a-=e}if(this.size-b<25){var h=[];this.collapse(h),this.children=[new de(h)],this.children[0].parent=this}},collapse:function(a){for(var b=0,c=this.children.length;c>b;++b)this.children[b].collapse(a)},insertInner:function(a,b,c){this.size+=b.length,this.height+=c;for(var d=0,e=this.children.length;e>d;++d){var f=this.children[d],g=f.chunkSize();if(g>=a){if(f.insertInner(a,b,c),f.lines&&f.lines.length>50){for(;f.lines.length>50;){var h=f.lines.splice(f.lines.length-25,25),i=new de(h);f.height-=i.height,this.children.splice(d+1,0,i),i.parent=this}this.maybeSpill()}break}a-=g}},maybeSpill:function(){if(!(this.children.length<=10)){var a=this;do{var b=a.children.splice(a.children.length-5,5),c=new ee(b);if(a.parent){a.size-=c.size,a.height-=c.height;var e=bf(a.parent.children,a);a.parent.children.splice(e+1,0,c)}else{var d=new ee(a.children);d.parent=a,a.children=[d,c],a=d}c.parent=a.parent}while(a.children.length>10);a.parent.maybeSpill()}},iterN:function(a,b,c){for(var d=0,e=this.children.length;e>d;++d){var f=this.children[d],g=f.chunkSize();if(g>a){var h=Math.min(b,g-a);if(f.iterN(a,h,c))return!0;if(0==(b-=h))break;a=0}else a-=g}}};var fe=0,ge=x.Doc=function(a,b,c){if(!(this instanceof ge))return new ge(a,b,c);null==c&&(c=0),ee.call(this,[new de([new Od("",null)])]),this.first=c,this.scrollTop=this.scrollLeft=0,this.cantEdit=!1,this.history=te(),this.cleanGeneration=1,this.frontier=c;var d=Bc(c,0);this.sel={from:d,to:d,head:d,anchor:d,shift:!1,extend:!1,goalColumn:null},this.id=++fe,this.modeOption=b,"string"==typeof a&&(a=wf(a)),ce(this,{from:d,to:d,text:a},null,{head:d,anchor:d})};ge.prototype=cf(ee.prototype,{constructor:ge,iter:function(a,b,c){c?this.iterN(a-this.first,b-a,c):this.iterN(this.first,this.first+this.size,a)},insert:function(a,b){for(var c=0,d=0,e=b.length;e>d;++d)c+=b[d].height;this.insertInner(a-this.first,b,c)},remove:function(a,b){this.removeInner(a-this.first,b)},getValue:function(a){var b=ne(this,this.first,this.first+this.size);return a===!1?b:b.join(a||"\n")},setValue:function(a){var b=Bc(this.first,0),c=this.first+this.size-1;uc(this,{from:b,to:Bc(c,le(this,c).text.length),text:wf(a),origin:"setValue"},{head:b,anchor:b},!0)},replaceRange:function(a,b,c,d){b=Gc(this,b),c=c?Gc(this,c):b,Ac(this,a,b,c,d)},getRange:function(a,b,c){var d=me(this,Gc(this,a),Gc(this,b));return c===!1?d:d.join(c||"\n")},getLine:function(a){var b=this.getLineHandle(a);return b&&b.text},setLine:function(a,b){Ic(this,a)&&Ac(this,b,Bc(a,0),Gc(this,Bc(a)))},removeLine:function(a){a?Ac(this,"",Gc(this,Bc(a-1)),Gc(this,Bc(a))):Ac(this,"",Bc(0,0),Gc(this,Bc(1,0)))},getLineHandle:function(a){return Ic(this,a)?le(this,a):void 0},getLineNumber:function(a){return pe(a)},getLineHandleVisualStart:function(a){return"number"==typeof a&&(a=le(this,a)),Fd(this,a)},lineCount:function(){return this.size},firstLine:function(){return this.first},lastLine:function(){return this.first+this.size-1},clipPos:function(a){return Gc(this,a)},getCursor:function(a){var c,b=this.sel;return c=null==a||"head"==a?b.head:"anchor"==a?b.anchor:"end"==a||a===!1?b.to:b.from,Ec(c)},somethingSelected:function(){return!Cc(this.sel.head,this.sel.anchor)},setCursor:Gb(function(a,b,c){var d=Gc(this,"number"==typeof a?Bc(a,b||0):a);c?Jc(this,d):Lc(this,d,d)}),setSelection:Gb(function(a,b,c){Lc(this,Gc(this,a),Gc(this,b||a),c)}),extendSelection:Gb(function(a,b,c){Jc(this,Gc(this,a),b&&Gc(this,b),c)}),getSelection:function(a){return this.getRange(this.sel.from,this.sel.to,a)},replaceSelection:function(a,b,c){uc(this,{from:this.sel.from,to:this.sel.to,text:wf(a),origin:c},b||"around")},undo:Gb(function(){wc(this,"undo")}),redo:Gb(function(){wc(this,"redo")}),setExtending:function(a){this.sel.extend=a},historySize:function(){var a=this.history;return{undo:a.done.length,redo:a.undone.length}},clearHistory:function(){this.history=te(this.history.maxGeneration)},markClean:function(){this.cleanGeneration=this.changeGeneration()},changeGeneration:function(){return this.history.lastOp=this.history.lastOrigin=null,this.history.generation},isClean:function(a){return this.history.generation==(a||this.cleanGeneration)},getHistory:function(){return{done:ze(this.history.done),undone:ze(this.history.undone)}},setHistory:function(a){var b=this.history=te(this.history.maxGeneration);b.done=a.done.slice(0),b.undone=a.undone.slice(0)},markText:function(a,b,c){return rd(this,Gc(this,a),Gc(this,b),c,"range")},setBookmark:function(a,b){var c={replacedWith:b&&(null==b.nodeType?b.widget:b),insertLeft:b&&b.insertLeft};return a=Gc(this,a),rd(this,a,a,c,"bookmark")},findMarksAt:function(a){a=Gc(this,a);var b=[],c=le(this,a.line).markedSpans;if(c)for(var d=0;d<c.length;++d){var e=c[d];(null==e.from||e.from<=a.ch)&&(null==e.to||e.to>=a.ch)&&b.push(e.marker.parent||e.marker)}return b},getAllMarks:function(){var a=[];return this.iter(function(b){var c=b.markedSpans;if(c)for(var d=0;d<c.length;++d)null!=c[d].from&&a.push(c[d].marker)}),a},posFromIndex:function(a){var b,c=this.first;return this.iter(function(d){var e=d.text.length+1;return e>a?(b=a,!0):(a-=e,++c,void 0)}),Gc(this,Bc(c,b))},indexFromPos:function(a){a=Gc(this,a);var b=a.ch;return a.line<this.first||a.ch<0?0:(this.iter(this.first,a.line,function(a){b+=a.text.length+1}),b)},copy:function(a){var b=new ge(ne(this,this.first,this.first+this.size),this.modeOption,this.first);return b.scrollTop=this.scrollTop,b.scrollLeft=this.scrollLeft,b.sel={from:this.sel.from,to:this.sel.to,head:this.sel.head,anchor:this.sel.anchor,shift:this.sel.shift,extend:!1,goalColumn:this.sel.goalColumn},a&&(b.history.undoDepth=this.history.undoDepth,b.setHistory(this.getHistory())),b},linkedDoc:function(a){a||(a={});var b=this.first,c=this.first+this.size;null!=a.from&&a.from>b&&(b=a.from),null!=a.to&&a.to<c&&(c=a.to);var d=new ge(ne(this,b,c),a.mode||this.modeOption,b);return a.sharedHist&&(d.history=this.history),(this.linked||(this.linked=[])).push({doc:d,sharedHist:a.sharedHist}),d.linked=[{doc:this,isParent:!0,sharedHist:a.sharedHist}],d},unlinkDoc:function(a){if(a instanceof x&&(a=a.doc),this.linked)for(var b=0;b<this.linked.length;++b){var c=this.linked[b];if(c.doc==a){this.linked.splice(b,1),a.unlinkDoc(this);break}}if(a.history==this.history){var d=[a.id];je(a,function(a){d.push(a.id)},!0),a.history=te(),a.history.done=ze(this.history.done,d),a.history.undone=ze(this.history.undone,d)}},iterLinkedDocs:function(a){je(this,a)},getMode:function(){return this.mode},getEditor:function(){return this.cm}}),ge.prototype.eachLine=ge.prototype.iter;var he="iter insert remove copy getEditor".split(" ");for(var ie in ge.prototype)ge.prototype.hasOwnProperty(ie)&&bf(he,ie)<0&&(x.prototype[ie]=function(a){return function(){return a.apply(this.doc,arguments)}}(ge.prototype[ie]));Ue(ge),x.e_stop=Ie,x.e_preventDefault=Fe,x.e_stopPropagation=Ge;var Oe,Pe=0;x.on=Le,x.off=Me,x.signal=Ne;var Ve=30,We=x.Pass={toString:function(){return"CodeMirror.Pass"}};Xe.prototype={set:function(a,b){clearTimeout(this.id),this.id=setTimeout(b,a)}},x.countColumn=Ye;var Ze=[""],gf=/[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/,kf=/[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\uA66F\uA670-\uA672\uA674-\uA67D\uA69F\udc00-\udfff]/;x.replaceGetRect=function(a){pf=a};var qf=function(){if(d)return!1;var a=lf("div");return"draggable"in a||"dragDrop"in a}();a?rf=function(a,b){return 36==a.charCodeAt(b-1)&&39==a.charCodeAt(b)}:j&&!/Version\/([6-9]|\d\d)\b/.test(navigator.userAgent)?rf=function(a,b){return/\-[^ \-?]|\?[^ !\'\"\),.\-\/:;\?\]\}]/.test(a.slice(b-1,b+1))}:f&&/Chrome\/(?:29|[3-9]\d|\d\d\d)\./.test(navigator.userAgent)?rf=function(a,b){var c=a.charCodeAt(b-1);return c>=8208&&8212>=c}:f&&(rf=function(a,b){if(b>1&&45==a.charCodeAt(b-1)){if(/\w/.test(a.charAt(b-2))&&/[^\-?\.]/.test(a.charAt(b)))return!0;if(b>2&&/[\d\.,]/.test(a.charAt(b-2))&&/[\d\.,]/.test(a.charAt(b)))return!1}return/[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|\u2026[\w~`@#$%\^&*(_=+{[><]/.test(a.slice(b-1,b+1))});var sf,uf,wf=3!="\n\nb".split(/\n/).length?function(a){for(var b=0,c=[],d=a.length;d>=b;){var e=a.indexOf("\n",b);-1==e&&(e=a.length);var f=a.slice(b,"\r"==a.charAt(e-1)?e-1:e),g=f.indexOf("\r");-1!=g?(c.push(f.slice(0,g)),b+=g+1):(c.push(f),b=e+1)}return c}:function(a){return a.split(/\r\n?|\n/)};x.splitLines=wf;var xf=window.getSelection?function(a){try{return a.selectionStart!=a.selectionEnd}catch(b){return!1}}:function(a){try{var b=a.ownerDocument.selection.createRange()}catch(c){}return b&&b.parentElement()==a?0!=b.compareEndPoints("StartToEnd",b):!1},yf=function(){var a=lf("div");return"oncopy"in a?!0:(a.setAttribute("oncopy","return;"),"function"==typeof a.oncopy)}(),zf={3:"Enter",8:"Backspace",9:"Tab",13:"Enter",16:"Shift",17:"Ctrl",18:"Alt",19:"Pause",20:"CapsLock",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"PrintScrn",45:"Insert",46:"Delete",59:";",91:"Mod",92:"Mod",93:"Mod",109:"-",107:"=",127:"Delete",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",63276:"PageUp",63277:"PageDown",63275:"End",63273:"Home",63234:"Left",63232:"Up",63235:"Right",63233:"Down",63302:"Insert",63272:"Delete"};x.keyNames=zf,function(){for(var a=0;10>a;a++)zf[a+48]=String(a);for(var a=65;90>=a;a++)zf[a]=String.fromCharCode(a);for(var a=1;12>=a;a++)zf[a+111]=zf[a+63235]="F"+a}();var If,Nf=function(){function c(c){return 255>=c?a.charAt(c):c>=1424&&1524>=c?"R":c>=1536&&1791>=c?b.charAt(c-1536):c>=1792&&2220>=c?"r":"L"}var a="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLL",b="rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmmrrrrrrrrrrrrrrrrrr",d=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,e=/[stwN]/,f=/[LRr]/,g=/[Lb1n]/,h=/[1n]/,i="L";return function(a){if(!d.test(a))return!1;for(var l,b=a.length,j=[],k=0;b>k;++k)j.push(l=c(a.charCodeAt(k)));for(var k=0,m=i;b>k;++k){var l=j[k];"m"==l?j[k]=m:m=l}for(var k=0,n=i;b>k;++k){var l=j[k];"1"==l&&"r"==n?j[k]="n":f.test(l)&&(n=l,"r"==l&&(j[k]="R"))}for(var k=1,m=j[0];b-1>k;++k){var l=j[k];"+"==l&&"1"==m&&"1"==j[k+1]?j[k]="1":","!=l||m!=j[k+1]||"1"!=m&&"n"!=m||(j[k]=m),m=l}for(var k=0;b>k;++k){var l=j[k];if(","==l)j[k]="N";else if("%"==l){for(var o=k+1;b>o&&"%"==j[o];++o);for(var p=k&&"!"==j[k-1]||b-1>o&&"1"==j[o]?"1":"N",q=k;o>q;++q)j[q]=p;k=o-1}}for(var k=0,n=i;b>k;++k){var l=j[k];"L"==n&&"1"==l?j[k]="L":f.test(l)&&(n=l)}for(var k=0;b>k;++k)if(e.test(j[k])){for(var o=k+1;b>o&&e.test(j[o]);++o);for(var r="L"==(k?j[k-1]:i),s="L"==(b-1>o?j[o]:i),p=r||s?"L":"R",q=k;o>q;++q)j[q]=p;k=o-1}for(var u,t=[],k=0;b>k;)if(g.test(j[k])){var v=k;for(++k;b>k&&g.test(j[k]);++k);t.push({from:v,to:k,level:0})}else{var w=k,x=t.length;for(++k;b>k&&"L"!=j[k];++k);for(var q=w;k>q;)if(h.test(j[q])){q>w&&t.splice(x,0,{from:w,to:q,level:1});var y=q;for(++q;k>q&&h.test(j[q]);++q);t.splice(x,0,{from:y,to:q,level:2}),w=q}else++q;k>w&&t.splice(x,0,{from:w,to:k,level:1})}return 1==t[0].level&&(u=a.match(/^\s+/))&&(t[0].from=u[0].length,t.unshift({from:0,to:u[0].length,level:0})),1==_e(t).level&&(u=a.match(/\s+$/))&&(_e(t).to-=u[0].length,t.push({from:b-u[0].length,to:b,level:0})),t[0].level!=_e(t).level&&t.push({from:b,to:b,level:t[0].level}),t}}();return x.version="3.19.1",x}(),CodeMirror.defineMode("css",function(a,b){"use strict";function l(a,b){return k=b,a}function m(a,b){var c=a.next();if(d[c]){var e=d[c](a,b);if(e!==!1)return e}if("@"==c)return a.eatWhile(/[\w\\\-]/),l("def",a.current());if("="==c)l(null,"compare");else{if(("~"==c||"|"==c)&&a.eat("="))return l(null,"compare");if('"'==c||"'"==c)return b.tokenize=n(c),b.tokenize(a,b);if("#"==c)return a.eatWhile(/[\w\\\-]/),l("atom","hash");if("!"==c)return a.match(/^\s*\w*/),l("keyword","important");if(/\d/.test(c)||"."==c&&a.eat(/\d/))return a.eatWhile(/[\w.%]/),l("number","unit");if("-"!==c)return/[,+>*\/]/.test(c)?l(null,"select-op"):"."==c&&a.match(/^-?[_a-z][_a-z0-9-]*/i)?l("qualifier","qualifier"):":"==c?l("operator",c):/[;{}\[\]\(\)]/.test(c)?l(null,c):"u"==c&&a.match("rl(")?(a.backUp(1),b.tokenize=o,l("property","variable")):(a.eatWhile(/[\w\\\-]/),l("property","variable"));if(/\d/.test(a.peek()))return a.eatWhile(/[\w.%]/),l("number","unit");if(a.match(/^[^-]+-/))return l("meta","meta")}}function n(a,b){return function(c,d){for(var f,e=!1;null!=(f=c.next())&&(f!=a||e);)e=!e&&"\\"==f;return e||(b&&c.backUp(1),d.tokenize=m),l("string","string")}}function o(a,b){return a.next(),b.tokenize=a.match(/\s*[\"\']/,!1)?m:n(")",!0),l(null,"(")}b.propertyKeywords||(b=CodeMirror.resolveMode("text/css"));var c=a.indentUnit||a.tabSize||2,d=b.hooks||{},e=b.atMediaTypes||{},f=b.atMediaFeatures||{},g=b.propertyKeywords||{},h=b.colorKeywords||{},i=b.valueKeywords||{},j=!!b.allowNested,k=null;return{startState:function(a){return{tokenize:m,baseIndent:a||0,stack:[],lastToken:null}},token:function(a,b){if(b.tokenize=b.tokenize||m,b.tokenize==m&&a.eatSpace())return null;var c=b.tokenize(a,b);c&&"string"!=typeof c&&(c=l(c[0],c[1]));var d=b.stack[b.stack.length-1];if("variable"==c)return"variable-definition"==k&&b.stack.push("propertyValue"),b.lastToken="variable-2";if("property"==c){var n=a.current().toLowerCase();"propertyValue"==d?c=i.hasOwnProperty(n)?"string-2":h.hasOwnProperty(n)?"keyword":"variable-2":"rule"==d?g.hasOwnProperty(n)||(c+=" error"):"block"==d?c=g.hasOwnProperty(n)?"property":h.hasOwnProperty(n)?"keyword":i.hasOwnProperty(n)?"string-2":"tag":d&&"@media{"!=d?"@media"==d?c=e[a.current()]?"attribute":/^(only|not)$/.test(n)?"keyword":"and"==n?"error":f.hasOwnProperty(n)?"error":"attribute error":"@mediaType"==d?c=e.hasOwnProperty(n)?"attribute":"and"==n?"operator":/^(only|not)$/.test(n)?"error":"error":"@mediaType("==d?g.hasOwnProperty(n)||(e.hasOwnProperty(n)?c="error":"and"==n?c="operator":/^(only|not)$/.test(n)?c="error":c+=" error"):c="@import"==d?"tag":"error":c="tag"}else"atom"==c?d&&"@media{"!=d&&"block"!=d?"propertyValue"==d?/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(a.current())||(c+=" error"):c="error":c="builtin":"@media"==d&&"{"==k&&(c="error");if("{"==k)if("@media"==d||"@mediaType"==d)b.stack[b.stack.length-1]="@media{";else{var o=j?"block":"rule";b.stack.push(o)}else if("}"==k)for("interpolation"==d&&(c="operator");b.stack.length;){var p=b.stack.pop();if(p.indexOf("{")>-1)break}else if("interpolation"==k)b.stack.push("interpolation");else if("@media"==k)b.stack.push("@media");else if("@import"==k)b.stack.push("@import");else if("@media"==d&&/\b(keyword|attribute)\b/.test(c))b.stack[b.stack.length-1]="@mediaType";else if("@mediaType"==d&&","==a.current())b.stack[b.stack.length-1]="@media";else if("("==k)"@media"==d||"@mediaType"==d?(b.stack[b.stack.length-1]="@mediaType",b.stack.push("@mediaType(")):b.stack.push("(");else if(")"==k)for(;b.stack.length;){var p=b.stack.pop();if(p.indexOf("(")>-1)break}else":"==k&&"property"==b.lastToken?b.stack.push("propertyValue"):"propertyValue"==d&&";"==k?b.stack.pop():"@import"==d&&";"==k&&b.stack.pop();return b.lastToken=c},indent:function(a,b){var d=a.stack.length;return/^\}/.test(b)&&(d-="propertyValue"==a.stack[d-1]?2:1),a.baseIndent+d*c},electricChars:"}",blockCommentStart:"/*",blockCommentEnd:"*/",fold:"brace"}}),function(){function a(a){for(var b={},c=0;c<a.length;++c)b[a[c]]=!0;return b}function g(a,b){for(var d,c=!1;null!=(d=a.next());){if(c&&"/"==d){b.tokenize=null;break}c="*"==d}return["comment","comment"]}var b=a(["all","aural","braille","handheld","print","projection","screen","tty","tv","embossed"]),c=a(["width","min-width","max-width","height","min-height","max-height","device-width","min-device-width","max-device-width","device-height","min-device-height","max-device-height","aspect-ratio","min-aspect-ratio","max-aspect-ratio","device-aspect-ratio","min-device-aspect-ratio","max-device-aspect-ratio","color","min-color","max-color","color-index","min-color-index","max-color-index","monochrome","min-monochrome","max-monochrome","resolution","min-resolution","max-resolution","scan","grid"]),d=a(["align-content","align-items","align-self","alignment-adjust","alignment-baseline","anchor-point","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","appearance","azimuth","backface-visibility","background","background-attachment","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","baseline-shift","binding","bleed","bookmark-label","bookmark-level","bookmark-state","bookmark-target","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","clear","clip","color","color-profile","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","content","counter-increment","counter-reset","crop","cue","cue-after","cue-before","cursor","direction","display","dominant-baseline","drop-initial-after-adjust","drop-initial-after-align","drop-initial-before-adjust","drop-initial-before-align","drop-initial-size","drop-initial-value","elevation","empty-cells","fit","fit-position","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","float-offset","flow-from","flow-into","font","font-feature-settings","font-family","font-kerning","font-language-override","font-size","font-size-adjust","font-stretch","font-style","font-synthesis","font-variant","font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-weight","grid-cell","grid-column","grid-column-align","grid-column-sizing","grid-column-span","grid-columns","grid-flow","grid-row","grid-row-align","grid-row-sizing","grid-row-span","grid-rows","grid-template","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","inline-box-align","justify-content","left","letter-spacing","line-break","line-height","line-stacking","line-stacking-ruby","line-stacking-shift","line-stacking-strategy","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marker-offset","marks","marquee-direction","marquee-loop","marquee-play-count","marquee-speed","marquee-style","max-height","max-width","min-height","min-width","move-to","nav-down","nav-index","nav-left","nav-right","nav-up","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-style","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page","page-break-after","page-break-before","page-break-inside","page-policy","pause","pause-after","pause-before","perspective","perspective-origin","pitch","pitch-range","play-during","position","presentation-level","punctuation-trim","quotes","region-break-after","region-break-before","region-break-inside","region-fragment","rendering-intent","resize","rest","rest-after","rest-before","richness","right","rotation","rotation-point","ruby-align","ruby-overhang","ruby-position","ruby-span","shape-inside","shape-outside","size","speak","speak-as","speak-header","speak-numeral","speak-punctuation","speech-rate","stress","string-set","tab-size","table-layout","target","target-name","target-new","target-position","text-align","text-align-last","text-decoration","text-decoration-color","text-decoration-line","text-decoration-skip","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-height","text-indent","text-justify","text-outline","text-overflow","text-shadow","text-size-adjust","text-space-collapse","text-transform","text-underline-position","text-wrap","top","transform","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","volume","white-space","widows","width","word-break","word-spacing","word-wrap","z-index","zoom","clip-path","clip-rule","mask","enable-background","filter","flood-color","flood-opacity","lighting-color","stop-color","stop-opacity","pointer-events","color-interpolation","color-interpolation-filters","color-profile","color-rendering","fill","fill-opacity","fill-rule","image-rendering","marker","marker-end","marker-mid","marker-start","shape-rendering","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","text-rendering","baseline-shift","dominant-baseline","glyph-orientation-horizontal","glyph-orientation-vertical","kerning","text-anchor","writing-mode"]),e=a(["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"]),f=a(["above","absolute","activeborder","activecaption","afar","after-white-space","ahead","alias","all","all-scroll","alternate","always","amharic","amharic-abegede","antialiased","appworkspace","arabic-indic","armenian","asterisks","auto","avoid","avoid-column","avoid-page","avoid-region","background","backwards","baseline","below","bidi-override","binary","bengali","blink","block","block-axis","bold","bolder","border","border-box","both","bottom","break","break-all","break-word","button","button-bevel","buttonface","buttonhighlight","buttonshadow","buttontext","cambodian","capitalize","caps-lock-indicator","caption","captiontext","caret","cell","center","checkbox","circle","cjk-earthly-branch","cjk-heavenly-stem","cjk-ideographic","clear","clip","close-quote","col-resize","collapse","column","compact","condensed","contain","content","content-box","context-menu","continuous","copy","cover","crop","cross","crosshair","currentcolor","cursive","dashed","decimal","decimal-leading-zero","default","default-button","destination-atop","destination-in","destination-out","destination-over","devanagari","disc","discard","document","dot-dash","dot-dot-dash","dotted","double","down","e-resize","ease","ease-in","ease-in-out","ease-out","element","ellipse","ellipsis","embed","end","ethiopic","ethiopic-abegede","ethiopic-abegede-am-et","ethiopic-abegede-gez","ethiopic-abegede-ti-er","ethiopic-abegede-ti-et","ethiopic-halehame-aa-er","ethiopic-halehame-aa-et","ethiopic-halehame-am-et","ethiopic-halehame-gez","ethiopic-halehame-om-et","ethiopic-halehame-sid-et","ethiopic-halehame-so-et","ethiopic-halehame-ti-er","ethiopic-halehame-ti-et","ethiopic-halehame-tig","ew-resize","expanded","extra-condensed","extra-expanded","fantasy","fast","fill","fixed","flat","footnotes","forwards","from","geometricPrecision","georgian","graytext","groove","gujarati","gurmukhi","hand","hangul","hangul-consonant","hebrew","help","hidden","hide","higher","highlight","highlighttext","hiragana","hiragana-iroha","horizontal","hsl","hsla","icon","ignore","inactiveborder","inactivecaption","inactivecaptiontext","infinite","infobackground","infotext","inherit","initial","inline","inline-axis","inline-block","inline-table","inset","inside","intrinsic","invert","italic","justify","kannada","katakana","katakana-iroha","keep-all","khmer","landscape","lao","large","larger","left","level","lighter","line-through","linear","lines","list-item","listbox","listitem","local","logical","loud","lower","lower-alpha","lower-armenian","lower-greek","lower-hexadecimal","lower-latin","lower-norwegian","lower-roman","lowercase","ltr","malayalam","match","media-controls-background","media-current-time-display","media-fullscreen-button","media-mute-button","media-play-button","media-return-to-realtime-button","media-rewind-button","media-seek-back-button","media-seek-forward-button","media-slider","media-sliderthumb","media-time-remaining-display","media-volume-slider","media-volume-slider-container","media-volume-sliderthumb","medium","menu","menulist","menulist-button","menulist-text","menulist-textfield","menutext","message-box","middle","min-intrinsic","mix","mongolian","monospace","move","multiple","myanmar","n-resize","narrower","ne-resize","nesw-resize","no-close-quote","no-drop","no-open-quote","no-repeat","none","normal","not-allowed","nowrap","ns-resize","nw-resize","nwse-resize","oblique","octal","open-quote","optimizeLegibility","optimizeSpeed","oriya","oromo","outset","outside","outside-shape","overlay","overline","padding","padding-box","painted","page","paused","persian","plus-darker","plus-lighter","pointer","polygon","portrait","pre","pre-line","pre-wrap","preserve-3d","progress","push-button","radio","read-only","read-write","read-write-plaintext-only","rectangle","region","relative","repeat","repeat-x","repeat-y","reset","reverse","rgb","rgba","ridge","right","round","row-resize","rtl","run-in","running","s-resize","sans-serif","scroll","scrollbar","se-resize","searchfield","searchfield-cancel-button","searchfield-decoration","searchfield-results-button","searchfield-results-decoration","semi-condensed","semi-expanded","separate","serif","show","sidama","single","skip-white-space","slide","slider-horizontal","slider-vertical","sliderthumb-horizontal","sliderthumb-vertical","slow","small","small-caps","small-caption","smaller","solid","somali","source-atop","source-in","source-out","source-over","space","square","square-button","start","static","status-bar","stretch","stroke","sub","subpixel-antialiased","super","sw-resize","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","telugu","text","text-bottom","text-top","textarea","textfield","thai","thick","thin","threeddarkshadow","threedface","threedhighlight","threedlightshadow","threedshadow","tibetan","tigre","tigrinya-er","tigrinya-er-abegede","tigrinya-et","tigrinya-et-abegede","to","top","transparent","ultra-condensed","ultra-expanded","underline","up","upper-alpha","upper-armenian","upper-greek","upper-hexadecimal","upper-latin","upper-norwegian","upper-roman","uppercase","urdu","url","vertical","vertical-text","visible","visibleFill","visiblePainted","visibleStroke","visual","w-resize","wait","wave","wider","window","windowframe","windowtext","x-large","x-small","xor","xx-large","xx-small"]);CodeMirror.defineMIME("text/css",{atMediaTypes:b,atMediaFeatures:c,propertyKeywords:d,colorKeywords:e,valueKeywords:f,hooks:{"<":function(a,b){function c(a,b){for(var d,c=0;null!=(d=a.next());){if(c>=2&&">"==d){b.tokenize=null;break}c="-"==d?c+1:0}return["comment","comment"]}return a.eat("!")?(b.tokenize=c,c(a,b)):void 0},"/":function(a,b){return a.eat("*")?(b.tokenize=g,g(a,b)):!1}},name:"css"}),CodeMirror.defineMIME("text/x-scss",{atMediaTypes:b,atMediaFeatures:c,propertyKeywords:d,colorKeywords:e,valueKeywords:f,allowNested:!0,hooks:{":":function(a){return a.match(/\s*{/)?[null,"{"]:!1},$:function(a){return a.match(/^[\w-]+/),":"==a.peek()?["variable","variable-definition"]:["variable","variable"]},",":function(a,b){return"propertyValue"==b.stack[b.stack.length-1]?["operator",";"]:void 0},"/":function(a,b){return a.eat("/")?(a.skipToEnd(),["comment","comment"]):a.eat("*")?(b.tokenize=g,g(a,b)):["operator","operator"]
+},"#":function(a){return a.eat("{")?["operator","interpolation"]:(a.eatWhile(/[\w\\\-]/),["atom","hash"])}},name:"css"})}(),CodeMirror.defineMode("less",function(a){function d(a,b){return c=b,a}function f(a,b){var f=a.next();if("@"==f)return a.eatWhile(/[\w\-]/),d("meta",a.current());if("/"==f&&a.eat("*"))return b.tokenize=h,h(a,b);if("<"==f&&a.eat("!"))return b.tokenize=i,i(a,b);if("="==f)d(null,"compare");else{if("|"==f&&a.eat("="))return d(null,"compare");if('"'==f||"'"==f)return b.tokenize=j(f),b.tokenize(a,b);if("/"==f){if(a.eat("/"))return b.tokenize=g,g(a,b);if("string"==c||"("==c)return d("string","string");if(void 0!==b.stack[b.stack.length-1])return d(null,f);if(a.eatWhile(/[\a-zA-Z0-9\-_.\s]/),/\/|\)|#/.test(a.peek()||a.eatSpace()&&")"===a.peek())||a.eol())return d("string","string")}else{if("!"==f)return a.match(/^\s*\w*/),d("keyword","important");if(/\d/.test(f))return a.eatWhile(/[\w.%]/),d("number","unit");if(/[,+<>*\/]/.test(f))return"="==a.peek()||"a"==c?d("string","string"):","===f?d(null,f):d(null,"select-op");if(/[;{}:\[\]()~\|]/.test(f)){if(":"==f)return a.eatWhile(/[a-z\\\-]/),e.test(a.current())?d("tag","tag"):":"==a.peek()?(a.next(),a.eatWhile(/[a-z\\\-]/),a.current().match(/\:\:\-(o|ms|moz|webkit)\-/)?d("string","string"):e.test(a.current().substring(1))?d("tag","tag"):d(null,f)):d(null,f);if("~"!=f)return d(null,f);if("r"==c)return d("string","string")}else{if("."==f)return"("==c?d("string","string"):(a.eatWhile(/[\a-zA-Z0-9\-_]/)," "===a.peek()&&a.eatSpace(),")"===a.peek()||":"===c?d("number","unit"):a.current().length>1&&"rule"===b.stack[b.stack.length-1]&&null===a.peek().match(/{|,|\+|\(/)?d("number","unit"):d("tag","tag"));if("#"==f)return a.eatWhile(/[A-Za-z0-9]/),4==a.current().length||7==a.current().length?null!=a.current().match(/[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/,!1)?a.current().substring(1)!=a.current().match(/[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/,!1)?d("atom","tag"):(a.eatSpace(),/[\/<>.(){!$%^&*_\-\\?=+\|#'~`]/.test(a.peek())?"select-op"===c?d("number","unit"):d("atom","tag"):"}"==a.peek()?d("number","unit"):/[a-zA-Z\\]/.test(a.peek())?d("atom","tag"):a.eol()?d("atom","tag"):d("number","unit")):(a.eatWhile(/[\w\\\-]/),d("atom",a.current())):(a.eatWhile(/[\w\\\-]/),"rule"===b.stack[b.stack.length-1]?d("atom",a.current()):d("atom",a.current()));if("&"==f)return a.eatWhile(/[\w\-]/),d(null,f);if(a.eatWhile(/[\w\\\-_%.{]/),null===a.current().match(/\\/)){if("string"==c)return"{"===b.stack[b.stack.length-1]&&":"===a.peek()?d("variable","variable"):("/"===a.peek()&&a.eatWhile(/[\w\\\-_%.{:\/]/),d(c,a.current()));if(null!=a.current().match(/(^http$|^https$)/))return a.eatWhile(/[\w\\\-_%.{:\/]/),"/"===a.peek()&&a.eatWhile(/[\w\\\-_%.{:\/]/),d("string","string");if("<"==a.peek()||">"==a.peek()||"+"==a.peek())return"("!==c||"n"!==a.current()&&"-n"!==a.current()?d("tag","tag"):d("string",a.current());if(/\(/.test(a.peek()))return"when"===a.current()?d("variable","variable"):"@media"===b.stack[b.stack.length-1]&&"and"===a.current()?d("variable",a.current()):d(null,f);if("/"==a.peek()&&void 0!==b.stack[b.stack.length-1])return"/"===a.peek()&&a.eatWhile(/[\w\\\-_%.{:\/]/),d("string",a.current());if(a.current().match(/\-\d|\-.\d/))return d("number","unit");if(/\/|[\s\)]/.test(a.peek()||a.eol()||a.eatSpace()&&"/"==a.peek())&&-1!==a.current().indexOf("."))return"{"==a.current().substring(a.current().length-1,a.current().length)?(a.backUp(1),d("tag","tag")):(a.eatSpace(),/[{<>.a-zA-Z\/]/.test(a.peek())||a.eol()?d("tag","tag"):d("string","string"));if(a.eol()||"["==a.peek()||"#"==a.peek()||"tag"==c){if("{"==a.current().substring(a.current().length-1,a.current().length))a.backUp(1);else{if("border-color"===b.stack[b.stack.length-1]||"background-position"===b.stack[b.stack.length-1]||"font-family"===b.stack[b.stack.length-1])return d(null,a.current());if("tag"===c)return d("tag","tag");if((":"===c||"unit"===c)&&"rule"===b.stack[b.stack.length-1])return d(null,a.current());if("rule"===b.stack[b.stack.length-1]&&"tag"===c)return d("string",a.current());if(";"===b.stack[b.stack.length-1]&&":"===c)return d(null,a.current());if("#"===a.peek()&&void 0!==c&&null===c.match(/\+|,|tag|select\-op|}|{|;/g))return d("string",a.current());if("variable"===c)return d(null,a.current());if("{"===b.stack[b.stack.length-1]&&"comment"===c)return d("variable",a.current());if(0===b.stack.length&&(";"===c||"comment"===c))return d("tag",a.current());if(("{"===b.stack[b.stack.length-1]||";"===c)&&"@media{"!==b.stack[b.stack.length-1])return d("variable",a.current());if("{"===b.stack[b.stack.length-2]&&";"===b.stack[b.stack.length-1])return d("variable",a.current())}return d("tag","tag")}if("compare"==c||"a"==c||"("==c)return d("string","string");if("|"==c||"-"==a.current()||"["==c)return"|"==c&&null!==a.peek().match(/\]|=|\~/)?d("number",a.current()):"|"==c?d("tag","tag"):"["==c?(a.eatWhile(/\w\-/),d("number",a.current())):d(null,f);if(":"==a.peek()||a.eatSpace()&&":"==a.peek()){a.next();var k=":"==a.peek()?!0:!1;if(k)a.backUp(1);else{var l=a.pos,m=a.current().length;a.eatWhile(/[a-z\\\-]/);var n=a.pos;if(null!=a.current().substring(m-1).match(e))return a.backUp(n-(l-1)),d("tag","tag");a.backUp(n-(l-1))}return k?d("tag","tag"):d("variable","variable")}return"font-family"===b.stack[b.stack.length-1]||"background-position"===b.stack[b.stack.length-1]||"border-color"===b.stack[b.stack.length-1]?d(null,null):null===b.stack[b.stack.length-1]&&":"===c?d(null,a.current()):/\^|\$/.test(a.current())&&null!==a.peek().match(/\~|=/)?d("string","string"):"unit"===c&&"rule"===b.stack[b.stack.length-1]?d(null,"unit"):"unit"===c&&";"===b.stack[b.stack.length-1]?d(null,"unit"):")"===c&&"rule"===b.stack[b.stack.length-1]?d(null,"unit"):c&&null!==c.match("@")&&"rule"===b.stack[b.stack.length-1]?d(null,"unit"):";"!==c&&"}"!==c&&","!==c||";"!==b.stack[b.stack.length-1]?";"===c&&void 0!==a.peek()&&null===a.peek().match(/{|./)||";"===c&&a.eatSpace()&&null===a.peek().match(/{|./)?d("variable",a.current()):"@media"===c&&"@media"===b.stack[b.stack.length-1]||"@namespace"===c?d("tag",a.current()):"{"===c&&";"===b.stack[b.stack.length-1]&&"{"===a.peek()?d("tag","tag"):"{"!==c&&":"!==c||";"!==b.stack[b.stack.length-1]?"{"===b.stack[b.stack.length-1]&&a.eatSpace()&&null===a.peek().match(/.|#/)||"select-op"===c||"rule"===b.stack[b.stack.length-1]&&","===c?d("tag","tag"):"variable"===c&&"rule"===b.stack[b.stack.length-1]?d("tag","tag"):a.eatSpace()&&"{"===a.peek()||a.eol()||"{"===a.peek()?d("tag","tag"):")"!==c||"and"!=a.current()&&"and "!=a.current()?")"!==c||"when"!=a.current()&&"when "!=a.current()?")"===c||"comment"===c||"{"===c?d("tag","tag"):a.sol()?d("tag","tag"):a.eatSpace()&&"#"===a.peek()||"#"===a.peek()?d("tag","tag"):0===b.stack.length?d("tag","tag"):";"===c&&void 0!==a.peek()&&null!==a.peek().match(/^[.|\#]/g)?d("tag","tag"):":"===c?(a.eatSpace(),d(null,a.current())):"and "===a.current()||"and"===a.current()?d("variable",a.current()):";"===c&&"{"===b.stack[b.stack.length-1]?d("variable",a.current()):"rule"===b.stack[b.stack.length-1]?d(null,a.current()):d("tag",a.current()):d("variable","variable"):d("variable","variable"):d(null,a.current()):d("tag",a.current())}if("\\"===a.current().charAt(a.current().length-1)){for(a.eat(/\'|\"|\)|\(/);a.eatWhile(/[\w\\\-_%.{]/);)a.eat(/\'|\"|\)|\(/);return d("string",a.current())}}}}}function g(a,b){return a.skipToEnd(),b.tokenize=f,d("comment","comment")}function h(a,b){for(var e,c=!1;null!=(e=a.next());){if(c&&"/"==e){b.tokenize=f;break}c="*"==e}return d("comment","comment")}function i(a,b){for(var e,c=0;null!=(e=a.next());){if(c>=2&&">"==e){b.tokenize=f;break}c="-"==e?c+1:0}return d("comment","comment")}function j(a){return function(b,c){for(var g,e=!1;null!=(g=b.next())&&(g!=a||e);)e=!e&&"\\"==g;return e||(c.tokenize=f),d("string","string")}}var c,b=a.indentUnit,e=/(^\:root$|^\:nth\-child$|^\:nth\-last\-child$|^\:nth\-of\-type$|^\:nth\-last\-of\-type$|^\:first\-child$|^\:last\-child$|^\:first\-of\-type$|^\:last\-of\-type$|^\:only\-child$|^\:only\-of\-type$|^\:empty$|^\:link|^\:visited$|^\:active$|^\:hover$|^\:focus$|^\:target$|^\:lang$|^\:enabled^\:disabled$|^\:checked$|^\:first\-line$|^\:first\-letter$|^\:before$|^\:after$|^\:not$|^\:required$|^\:invalid$)/;return{startState:function(a){return{tokenize:f,baseIndent:a||0,stack:[]}},token:function(a,b){if(a.eatSpace())return null;var e=b.tokenize(a,b),f=b.stack[b.stack.length-1];if("hash"==c&&"rule"==f?e="atom":"variable"==e&&("rule"==f?e=null:f&&"@media{"!=f||(e="when"==a.current()?"variable":/[\s,|\s\)|\s]/.test(a.peek())?"tag":c)),"rule"==f&&/^[\{\};]$/.test(c)&&b.stack.pop(),"{"==c?"@media"==f?b.stack[b.stack.length-1]="@media{":b.stack.push("{"):"}"==c?b.stack.pop():"@media"==c?b.stack.push("@media"):"font-family"===a.current()?b.stack[b.stack.length-1]="font-family":"background-position"===a.current()?b.stack[b.stack.length-1]="background-position":"border-color"===a.current()?b.stack[b.stack.length-1]="border-color":"{"==f&&"comment"!=c&&"tag"!==c?b.stack.push("rule"):":"===a.peek()&&null===a.current().match(/@|#/)&&(e=c),";"!==c||"font-family"!=b.stack[b.stack.length-1]&&"background-position"!=b.stack[b.stack.length-1]&&"border-color"!=b.stack[b.stack.length-1]){if("tag"===c&&")"===a.peek()&&null===a.current().match(/\:/))c=null,e=null;else if("variable"===c&&")"===a.peek()||"variable"===c&&a.eatSpace()&&")"===a.peek())return d(null,a.current())}else b.stack[b.stack.length-1]=a.current();return e},indent:function(a,c){var d=a.stack.length;return/^\}/.test(c)?d-="rule"===a.stack[a.stack.length-1]?2:1:"{"===a.stack[a.stack.length-2]&&(d-="rule"===a.stack[a.stack.length-1]?1:0),a.baseIndent+d*b},electricChars:"}",blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:"//"}}),CodeMirror.defineMIME("text/x-less","less"),CodeMirror.mimeModes.hasOwnProperty("text/css")||CodeMirror.defineMIME("text/css","less"); \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css-preview.js b/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css-preview.js
new file mode 100644
index 00000000..15090ee3
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css-preview.js
@@ -0,0 +1,42 @@
+// Originally based on https://raw.githubusercontent.com/xwp/wp-custom-scss-demo/master/custom-scss-demo-preview.js
+/* globals jpCustomizerCssPreview */
+(function( api, $ ) {
+ if ( api.settingPreviewHandlers ) {
+ // No-op the custom_css preview handler since now handled by partial.
+ api.settingPreviewHandlers.custom_css = function() {};
+ } else {
+ parent.console.warn( 'Missing core patch that adds support for settingPreviewHandlers' );
+ }
+
+ api.selectiveRefresh.partialConstructor.custom_css = api.selectiveRefresh.Partial.extend( {
+
+ /**
+ * Refresh custom_css partial, using selective refresh if pre-processor and direct DOM manipulation if otherwise.
+ *
+ * @returns {jQuery.promise}
+ */
+ refresh: function() {
+ var partial = this,
+ preprocessor = api( 'jetpack_custom_css[preprocessor]' ).get(),
+ deferred, setting;
+
+ // Sass or Less require Partial -- so ajax call to get it from PHP.
+ // We can explicitly override for specific providers by testing if `'sass' === preprocessor`
+ if ( jpCustomizerCssPreview.preprocessors.hasOwnProperty( preprocessor ) ) {
+ return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
+ }
+
+ // No special providers, just write what we got.
+ deferred = new $.Deferred();
+ setting = api( 'custom_css[' + api.settings.theme.stylesheet + ']' );
+ _.each( partial.placements(), function( placement ) {
+ placement.container.text( setting.get() );
+ } );
+
+ deferred.resolve();
+ return deferred.promise();
+ }
+
+ } );
+
+}( wp.customize, jQuery ));
diff --git a/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js b/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js
new file mode 100644
index 00000000..7fef365f
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js
@@ -0,0 +1,85 @@
+(function( $, customize ){
+ /**
+ * Helper function to qet a control by ID
+ * @param {string} controlId Control ID
+ * @return {object} jQuery object of the container
+ */
+ function _getControl ( controlId ) {
+ var control = customize.control.value( controlId );
+ if ( control ) {
+ return control.container;
+ }
+ return null;
+ }
+
+ /**
+ * Add some labels that the default checkbox controls don't allow.
+ * Add CSS Revisions and CSS Help links.
+ */
+ $(document).ready( function(){
+ var cssModeControl = _getControl( 'jetpack_css_mode_control' );
+ if ( cssModeControl ) {
+ cssModeControl.prepend( '<span class="customize-control-title">' + window._jp_css_settings.l10n.mode + '</span>' );
+ }
+
+ var mobileCssControl = _getControl( 'jetpack_mobile_css_control' );
+ if ( mobileCssControl ) {
+ mobileCssControl.prepend( '<span class="customize-control-title">' + window._jp_css_settings.l10n.mobile + '</span>' );
+ }
+
+ var widthControl = _getControl( 'wpcom_custom_css_content_width_control' );
+ if ( widthControl ) {
+ widthControl.append( '<span class="description">' + window._jp_css_settings.l10n.contentWidth + '<span>' );
+ widthControl.find( 'input' ).after( '<span>px</span>' );
+ }
+
+ $( '<div />', {
+ id : 'css-help-links',
+ 'class' : 'css-help'
+ }).appendTo( _getControl( 'custom_css' ) );
+
+ $( '<a />', {
+ id : 'help-link',
+ target : '_blank',
+ rel: 'noopener noreferrer',
+ href : window._jp_css_settings.cssHelpUrl,
+ text : window._jp_css_settings.l10n.css_help_title
+ }).prependTo( '#css-help-links' );
+
+ // Only show the revisions link if there are revisions
+ if ( window._jp_css_settings.areThereCssRevisions ) {
+ $( '<a />', {
+ id : 'revisions-link',
+ target : '_blank',
+ rel: 'noopener noreferrer',
+ href : window._jp_css_settings.revisionsUrl,
+ text : window._jp_css_settings.l10n.revisions
+ }).prependTo( '#css-help-links' );
+ }
+
+ customize( 'jetpack_custom_css[preprocessor]', function( preprocessorSetting ) {
+ preprocessorSetting.bind( function( curr ) {
+ var preprocessor_modes = {
+ 'default' : 'text/css',
+ less : 'text/x-less',
+ sass : 'text/x-scss'
+ },
+ new_mode = 'text/css';
+
+ if ( 'undefined' !== typeof preprocessor_modes[ curr ] ) {
+ new_mode = preprocessor_modes[ curr ];
+ }
+
+ customize.control( 'custom_css' ).deferred.codemirror.done( function ( cm ) {
+ cm.setOption( 'mode', new_mode );
+ if ( 'text/css' === new_mode ) {
+ cm.setOption( 'lint', true );
+ } else {
+ cm.setOption( 'lint', false );
+ }
+ });
+ });
+ });
+ });
+
+})( jQuery, this.wp.customize );
diff --git a/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.js b/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.js
new file mode 100644
index 00000000..570cb8d2
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.js
@@ -0,0 +1,192 @@
+(function( wp, $, api ){
+ api.controlConstructor.jetpackCss = api.Control.extend({
+ modes: {
+ 'default': 'text/css',
+ 'less': 'text/x-less',
+ 'sass': 'text/x-scss'
+ },
+ _updating: false,
+ /**
+ * Fires when our control is ready for action. Gets everything set up.
+ * @return {null}
+ */
+ ready: function() {
+ this.opts = window._jp_css_settings;
+ // add our textarea
+ this.$input = $( '<textarea />', {
+ name: this.setting.id,
+ 'class': 'for-codemirror hidden'
+ } ).val( this.setting() );
+ this.container.append( this.$input );
+
+ // keep the textarea and the setting synced up
+ api( this.setting.id, _.bind( function( setting ){
+ var element = new api.Element( this.$input );
+ this.elements = [ element ];
+ element.sync( setting );
+ element.set( setting() );
+ }, this ) );
+
+ // should we use CodeMirror?
+ if ( this.opts.useRichEditor ) {
+ this.initCodeMirror();
+ } else {
+ this.$input.removeClass( 'hidden' );
+ }
+
+ api.bind( 'ready', _.bind( this.addLabels, this ) );
+ },
+ /**
+ * Set up our CodeMirror instance
+ * @return {null}
+ */
+ initCodeMirror: function() {
+ this.editor = window.CodeMirror.fromTextArea( this.$input.get(0), {
+ mode: this.getMode(),
+ lineNumbers: true,
+ tabSize: 2,
+ indentWithTabs: true,
+ lineWrapping: true
+ } );
+
+ this.addListeners();
+ },
+ /**
+ * Adds various listeners for CodeMirror to render and keep in sync
+ * with the textarea.
+ */
+ addListeners: function() {
+ var edited = false;
+
+ // refresh the CodeMirror instance's rendering because it's initially hidden
+ // 250ms because that's the open animation duration
+ $( '#accordion-section-custom_css > .accordion-section-title' ).click( _.bind( _.debounce( this.editor.refresh, 250 ), this.editor ) );
+ // also refresh when focusing
+ this.editor.on( 'focus', function( editor ) {
+ editor.refresh();
+ });
+
+ // when the CodeMirror instance changes, mirror to the textarea,
+ // where we have our "true" change event handler bound. This allows both to function.
+ this.editor.on( 'change', _.bind( function( editor ) {
+ this._updating = true;
+ this.$input.val( editor.getValue() ).trigger( 'change' );
+ this._updating = false;
+
+ if ( ! edited ) {
+ window.ga && window.ga( 'send', 'event', 'Customizer', 'Typed Custom CSS' );
+ edited = true;
+ }
+ }, this ) );
+
+ this.editor.on( 'focus', function() {
+ window.ga && window.ga( 'send', 'event', 'Customizer', 'Focused CSS Editor' );
+ } );
+
+ // when others update the control, update CodeMirror
+ this.setting.bind( 'change', _.bind( this.externalChange, this ) );
+ },
+ /**
+ * Get the mode of the currently active preprocessor (if any),
+ * falling back to text/css
+ * @return {string} mode for CodeMirror
+ */
+ getMode: function() {
+ var mode = api( 'jetpack_custom_css[preprocessor]' )();
+ if ( '' === mode || ! this.modes[ mode ] ) {
+ mode = 'default';
+ }
+ return this.modes[ mode ];
+ },
+ /**
+ * If another control updates our setting, re-render the CodeMirror instance
+ * @return {null}
+ */
+ externalChange: function() {
+ // only if the change wasn't internal
+ if( ! this._updating ) {
+ this.editor.setValue( this.setting() );
+ }
+ },
+ /**
+ * Callback for when the CSS panel opens to refresh the CodeMirror rendering
+ * @param {string} id The panel being opened
+ * @return {null}
+ */
+ refresh: function( id ) {
+ if ( 'accordion-section-custom_css' === id ) {
+ setTimeout( _.bind( function(){
+ this.editor.refresh();
+ }, this), 300 );
+ }
+ },
+ /**
+ * Add some labels that the default checkbox controls don't allow.
+ * Add CSS Revisions and CSS Help links.
+ */
+ addLabels: function() {
+ this.addTitle( 'jetpack_css_mode_control', this.opts.l10n.mode );
+ this.addTitle( 'jetpack_mobile_css_control', this.opts.l10n.mobile );
+ this.addDesc( 'wpcom_custom_css_content_width_control', this.opts.l10n.contentWidth );
+ var widthControl = this._getControl( 'wpcom_custom_css_content_width_control' );
+ if ( widthControl ) {
+ widthControl.find( 'input' ).after( '<span>px</span>' );
+ }
+ $( '<div />', {
+ id: 'css-help-links',
+ 'class': 'css-help'
+ }).appendTo( this.container );
+ $( '<a />', {
+ id: 'help-link',
+ target: '_blank',
+ href: this.opts.cssHelpUrl,
+ text: this.opts.l10n.css_help_title
+ }).prependTo( '#css-help-links' );
+
+ // Only show the revisions link if there are revisions
+ if ( this.opts.areThereCssRevisions ) {
+ $( '<a />', {
+ id: 'revisions-link',
+ target: '_blank',
+ href: this.opts.revisionsUrl,
+ text: this.opts.l10n.revisions
+ }).prependTo( '#css-help-links' );
+ }
+ },
+ /**
+ * Add a title to a control
+ * @param {string} controlId Control ID
+ * @param {string} title A title to add
+ */
+ addTitle: function( controlId, title ) {
+ var control = this._getControl( controlId );
+ if ( control ) {
+ control.prepend( '<span class="customize-control-title">' + title + '<span>' );
+ }
+ },
+ /**
+ * Add a description to a control
+ * @param {string} controlId Control ID
+ * @param {string} desc A description to add
+ */
+ addDesc: function( controlId, desc ) {
+ var control = this._getControl( controlId );
+ if ( control ) {
+ control.append( '<span class="description">' + desc + '<span>' );
+ }
+ },
+ /**
+ * Helper function to qet a control by ID
+ * @param {string} controlId Control ID
+ * @return {object} jQuery object of the container
+ */
+ _getControl: function( controlId ) {
+ var control = api.control.value( controlId );
+ if ( control ) {
+ return control.container;
+ }
+ return null;
+ }
+ });
+
+})( this.wp, jQuery, this.wp.customize );
diff --git a/plugins/jetpack/modules/custom-css/custom-css/js/css-editor.js b/plugins/jetpack/modules/custom-css/custom-css/js/css-editor.js
new file mode 100644
index 00000000..9b17d73f
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/js/css-editor.js
@@ -0,0 +1,91 @@
+/* jshint onevar: false, smarttabs: true */
+/* global postboxes, addLoadEvent */
+
+( function( $ ) {
+ var safe, win, safecssResize, safecssInit;
+
+ safecssResize = function() {
+ safe.height( win.height() - safe.offset().top - 250 );
+ };
+
+ safecssInit = function() {
+ safe = $( '#safecss' );
+ win = $( window );
+
+ postboxes.add_postbox_toggles( 'editcss' );
+ safecssResize();
+
+ // Bound on a parent to ensure that this click event executes last.
+ $( '#safecssform' ).on( 'click', '#preview', function( e ) {
+ e.preventDefault();
+
+ document.forms.safecssform.target = 'csspreview';
+ document.forms.safecssform.action.value = 'preview';
+ document.forms.safecssform.submit();
+ document.forms.safecssform.target = '';
+ document.forms.safecssform.action.value = 'save';
+ } );
+ };
+
+ window.onresize = safecssResize;
+ addLoadEvent( safecssInit );
+} )( jQuery );
+
+jQuery( function( $ ) {
+ $( '.edit-preprocessor' ).bind( 'click', function( e ) {
+ e.preventDefault();
+
+ $( '#preprocessor-select' ).slideDown();
+ $( this ).hide();
+ } );
+
+ $( '.cancel-preprocessor' ).bind( 'click', function( e ) {
+ e.preventDefault();
+
+ $( '#preprocessor-select' ).slideUp( function() {
+ $( '.edit-preprocessor' ).show();
+ $( '#preprocessor_choices' ).val( $( '#custom_css_preprocessor' ).val() );
+ } );
+ } );
+
+ $( '.save-preprocessor' ).bind( 'click', function( e ) {
+ e.preventDefault();
+
+ $( '#preprocessor-select' ).slideUp();
+ $( '#preprocessor-display' ).text( $( '#preprocessor_choices option:selected' ).text() );
+ $( '#custom_css_preprocessor' )
+ .val( $( '#preprocessor_choices' ).val() )
+ .change();
+ $( '.edit-preprocessor' ).show();
+ } );
+
+ $( '.edit-css-mode' ).bind( 'click', function( e ) {
+ e.preventDefault();
+
+ $( '#css-mode-select' ).slideDown();
+ $( this ).hide();
+ } );
+
+ $( '.cancel-css-mode' ).bind( 'click', function( e ) {
+ e.preventDefault();
+
+ $( '#css-mode-select' ).slideUp( function() {
+ $( '.edit-css-mode' ).show();
+ $( 'input[name=add_to_existing_display][value=' + $( '#add_to_existing' ).val() + ']' ).attr(
+ 'checked',
+ true
+ );
+ } );
+ } );
+
+ $( '.save-css-mode' ).bind( 'click', function( e ) {
+ e.preventDefault();
+
+ $( '#css-mode-select' ).slideUp();
+ $( '#css-mode-display' ).text(
+ $( 'input[name=add_to_existing_display]:checked' ).val() === 'true' ? 'Add-on' : 'Replacement'
+ );
+ $( '#add_to_existing' ).val( $( 'input[name=add_to_existing_display]:checked' ).val() );
+ $( '.edit-css-mode' ).show();
+ } );
+} );
diff --git a/plugins/jetpack/modules/custom-css/custom-css/js/use-codemirror.js b/plugins/jetpack/modules/custom-css/custom-css/js/use-codemirror.js
new file mode 100644
index 00000000..60439308
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/js/use-codemirror.js
@@ -0,0 +1,52 @@
+/* jshint onevar: false, smarttabs: true */
+
+( function( $ ) {
+ var Jetpack_CSS = {
+ modes: {
+ default: 'text/css',
+ less: 'text/x-less',
+ sass: 'text/x-scss',
+ },
+ init: function() {
+ this.$textarea = $( '#safecss' );
+ this.editor = window.CodeMirror.fromTextArea( this.$textarea.get( 0 ), {
+ mode: this.getMode(),
+ lineNumbers: true,
+ tabSize: 2,
+ indentWithTabs: true,
+ lineWrapping: true,
+ } );
+ this.setEditorHeight();
+ },
+ addListeners: function() {
+ // nice sizing
+ $( window ).on( 'resize', _.bind( _.debounce( this.setEditorHeight, 100 ), this ) );
+ // keep textarea synced up
+ this.editor.on(
+ 'change',
+ _.bind( function( editor ) {
+ this.$textarea.val( editor.getValue() );
+ }, this )
+ );
+ // change mode
+ $( '#preprocessor_choices' ).change(
+ _.bind( function() {
+ this.editor.setOption( 'mode', this.getMode() );
+ }, this )
+ );
+ },
+ setEditorHeight: function() {
+ var height = $( 'html' ).height() - $( this.editor.getWrapperElement() ).offset().top;
+ this.editor.setSize( null, height );
+ },
+ getMode: function() {
+ var mode = $( '#preprocessor_choices' ).val();
+ if ( '' === mode || ! this.modes[ mode ] ) {
+ mode = 'default';
+ }
+ return this.modes[ mode ];
+ },
+ };
+
+ $( document ).ready( _.bind( Jetpack_CSS.init, Jetpack_CSS ) );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/custom-css/custom-css/preprocessors.php b/plugins/jetpack/modules/custom-css/custom-css/preprocessors.php
new file mode 100644
index 00000000..7d561b3d
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/preprocessors.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * CSS preprocessor registration.
+ *
+ * To add a new preprocessor (or replace an existing one), hook into the
+ * jetpack_custom_css_preprocessors filter and add an entry to the array
+ * that is passed in.
+ *
+ * Format is:
+ * $preprocessors[ UNIQUE_KEY ] => array( 'name' => 'Processor name', 'callback' => [processing function] );
+ *
+ * The callback function accepts a single string argument (non-CSS markup) and returns a string (CSS).
+ *
+ * @param array $preprocessors The list of preprocessors added thus far.
+ * @return array
+ */
+
+function jetpack_register_css_preprocessors( $preprocessors ) {
+ $preprocessors['less'] = array(
+ 'name' => 'LESS',
+ 'callback' => 'jetpack_less_css_preprocess'
+ );
+
+ $preprocessors['sass'] = array(
+ 'name' => 'Sass (SCSS Syntax)',
+ 'callback' => 'jetpack_sass_css_preprocess'
+ );
+
+ return $preprocessors;
+}
+
+add_filter( 'jetpack_custom_css_preprocessors', 'jetpack_register_css_preprocessors' );
+
+function jetpack_less_css_preprocess( $less ) {
+ require_once( dirname( __FILE__ ) . '/preprocessors/lessc.inc.php' );
+
+ $compiler = new lessc();
+
+ try {
+ return $compiler->compile( $less );
+ } catch ( Exception $e ) {
+ return $less;
+ }
+}
+
+function jetpack_sass_css_preprocess( $sass ) {
+ require_once( dirname( __FILE__ ) . '/preprocessors/scss.inc.php' );
+
+ $compiler = new scssc();
+ $compiler->setFormatter( 'scss_formatter' );
+
+ try {
+ return $compiler->compile( $sass );
+ } catch ( Exception $e ) {
+ return $sass;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-css/custom-css/preprocessors/lessc.inc.php b/plugins/jetpack/modules/custom-css/custom-css/preprocessors/lessc.inc.php
new file mode 100644
index 00000000..ddaa4788
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/preprocessors/lessc.inc.php
@@ -0,0 +1,3768 @@
+<?php
+
+/**
+ * lessphp v0.5.0
+ * http://leafo.net/lessphp
+ *
+ * LESS CSS compiler, adapted from http://lesscss.org
+ *
+ * Copyright 2013, Leaf Corcoran <leafot@gmail.com>
+ * Licensed under MIT or GPLv3, see LICENSE
+ */
+
+
+/**
+ * The LESS compiler and parser.
+ *
+ * Converting LESS to CSS is a three stage process. The incoming file is parsed
+ * by `lessc_parser` into a syntax tree, then it is compiled into another tree
+ * representing the CSS structure by `lessc`. The CSS tree is fed into a
+ * formatter, like `lessc_formatter` which then outputs CSS as a string.
+ *
+ * During the first compile, all values are *reduced*, which means that their
+ * types are brought to the lowest form before being dump as strings. This
+ * handles math equations, variable dereferences, and the like.
+ *
+ * The `parse` function of `lessc` is the entry point.
+ *
+ * In summary:
+ *
+ * The `lessc` class creates an instance of the parser, feeds it LESS code,
+ * then transforms the resulting tree to a CSS tree. This class also holds the
+ * evaluation context, such as all available mixins and variables at any given
+ * time.
+ *
+ * The `lessc_parser` class is only concerned with parsing its input.
+ *
+ * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
+ * handling things like indentation.
+ */
+class lessc {
+ static public $VERSION = "v0.5.0";
+
+ static public $TRUE = array("keyword", "true");
+ static public $FALSE = array("keyword", "false");
+
+ protected $libFunctions = array();
+ protected $registeredVars = array();
+ protected $preserveComments = false;
+
+ public $vPrefix = '@'; // prefix of abstract properties
+ public $mPrefix = '$'; // prefix of abstract blocks
+ public $parentSelector = '&';
+
+ public $importDisabled = false;
+ public $importDir = '';
+
+ protected $numberPrecision = null;
+
+ protected $allParsedFiles = array();
+
+ // set to the parser that generated the current line when compiling
+ // so we know how to create error messages
+ protected $sourceParser = null;
+ protected $sourceLoc = null;
+
+ static protected $nextImportId = 0; // uniquely identify imports
+
+ // attempts to find the path of an import url, returns null for css files
+ protected function findImport($url) {
+ foreach ((array)$this->importDir as $dir) {
+ $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
+ if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
+ return $file;
+ }
+ }
+
+ return null;
+ }
+
+ protected function fileExists($name) {
+ return is_file($name);
+ }
+
+ static public function compressList($items, $delim) {
+ if (!isset($items[1]) && isset($items[0])) return $items[0];
+ else return array('list', $delim, $items);
+ }
+
+ static public function preg_quote($what) {
+ return preg_quote($what, '/');
+ }
+
+ protected function tryImport($importPath, $parentBlock, $out) {
+ if ($importPath[0] == "function" && $importPath[1] == "url") {
+ $importPath = $this->flattenList($importPath[2]);
+ }
+
+ $str = $this->coerceString($importPath);
+ if ($str === null) return false;
+
+ $url = $this->compileValue($this->lib_e($str));
+
+ // don't import if it ends in css
+ if (substr_compare($url, '.css', -4, 4) === 0) return false;
+
+ $realPath = $this->findImport($url);
+
+ if ($realPath === null) return false;
+
+ if ($this->importDisabled) {
+ return array(false, "/* import disabled */");
+ }
+
+ if (isset($this->allParsedFiles[realpath($realPath)])) {
+ return array(false, null);
+ }
+
+ $this->addParsedFile($realPath);
+ $parser = $this->makeParser($realPath);
+ $root = $parser->parse(file_get_contents($realPath));
+
+ // set the parents of all the block props
+ foreach ($root->props as $prop) {
+ if ($prop[0] == "block") {
+ $prop[1]->parent = $parentBlock;
+ }
+ }
+
+ // copy mixins into scope, set their parents
+ // bring blocks from import into current block
+ // TODO: need to mark the source parser these came from this file
+ foreach ($root->children as $childName => $child) {
+ if (isset($parentBlock->children[$childName])) {
+ $parentBlock->children[$childName] = array_merge(
+ $parentBlock->children[$childName],
+ $child);
+ } else {
+ $parentBlock->children[$childName] = $child;
+ }
+ }
+
+ $pi = pathinfo($realPath);
+ $dir = $pi["dirname"];
+
+ list($top, $bottom) = $this->sortProps($root->props, true);
+ $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
+
+ return array(true, $bottom, $parser, $dir);
+ }
+
+ protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) {
+ $oldSourceParser = $this->sourceParser;
+
+ $oldImport = $this->importDir;
+
+ // TODO: this is because the importDir api is stupid
+ $this->importDir = (array)$this->importDir;
+ array_unshift($this->importDir, $importDir);
+
+ foreach ($props as $prop) {
+ $this->compileProp($prop, $block, $out);
+ }
+
+ $this->importDir = $oldImport;
+ $this->sourceParser = $oldSourceParser;
+ }
+
+ /**
+ * Recursively compiles a block.
+ *
+ * A block is analogous to a CSS block in most cases. A single LESS document
+ * is encapsulated in a block when parsed, but it does not have parent tags
+ * so all of it's children appear on the root level when compiled.
+ *
+ * Blocks are made up of props and children.
+ *
+ * Props are property instructions, array tuples which describe an action
+ * to be taken, eg. write a property, set a variable, mixin a block.
+ *
+ * The children of a block are just all the blocks that are defined within.
+ * This is used to look up mixins when performing a mixin.
+ *
+ * Compiling the block involves pushing a fresh environment on the stack,
+ * and iterating through the props, compiling each one.
+ *
+ * See lessc::compileProp()
+ *
+ */
+ protected function compileBlock($block) {
+ switch ($block->type) {
+ case "root":
+ $this->compileRoot($block);
+ break;
+ case null:
+ $this->compileCSSBlock($block);
+ break;
+ case "media":
+ $this->compileMedia($block);
+ break;
+ case "directive":
+ $name = "@" . $block->name;
+ if (!empty($block->value)) {
+ $name .= " " . $this->compileValue($this->reduce($block->value));
+ }
+
+ $this->compileNestedBlock($block, array($name));
+ break;
+ default:
+ $this->throwError("unknown block type: $block->type\n");
+ }
+ }
+
+ protected function compileCSSBlock($block) {
+ $env = $this->pushEnv();
+
+ $selectors = $this->compileSelectors($block->tags);
+ $env->selectors = $this->multiplySelectors($selectors);
+ $out = $this->makeOutputBlock(null, $env->selectors);
+
+ $this->scope->children[] = $out;
+ $this->compileProps($block, $out);
+
+ $block->scope = $env; // mixins carry scope with them!
+ $this->popEnv();
+ }
+
+ protected function compileMedia($media) {
+ $env = $this->pushEnv($media);
+ $parentScope = $this->mediaParent($this->scope);
+
+ $query = $this->compileMediaQuery($this->multiplyMedia($env));
+
+ $this->scope = $this->makeOutputBlock($media->type, array($query));
+ $parentScope->children[] = $this->scope;
+
+ $this->compileProps($media, $this->scope);
+
+ if (count($this->scope->lines) > 0) {
+ $orphanSelelectors = $this->findClosestSelectors();
+ if (!is_null($orphanSelelectors)) {
+ $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
+ $orphan->lines = $this->scope->lines;
+ array_unshift($this->scope->children, $orphan);
+ $this->scope->lines = array();
+ }
+ }
+
+ $this->scope = $this->scope->parent;
+ $this->popEnv();
+ }
+
+ protected function mediaParent($scope) {
+ while (!empty($scope->parent)) {
+ if (!empty($scope->type) && $scope->type != "media") {
+ break;
+ }
+ $scope = $scope->parent;
+ }
+
+ return $scope;
+ }
+
+ protected function compileNestedBlock($block, $selectors) {
+ $this->pushEnv($block);
+ $this->scope = $this->makeOutputBlock($block->type, $selectors);
+ $this->scope->parent->children[] = $this->scope;
+
+ $this->compileProps($block, $this->scope);
+
+ $this->scope = $this->scope->parent;
+ $this->popEnv();
+ }
+
+ protected function compileRoot($root) {
+ $this->pushEnv();
+ $this->scope = $this->makeOutputBlock($root->type);
+ $this->compileProps($root, $this->scope);
+ $this->popEnv();
+ }
+
+ protected function compileProps($block, $out) {
+ foreach ($this->sortProps($block->props) as $prop) {
+ $this->compileProp($prop, $block, $out);
+ }
+ $out->lines = $this->deduplicate($out->lines);
+ }
+
+ /**
+ * Deduplicate lines in a block. Comments are not deduplicated. If a
+ * duplicate rule is detected, the comments immediately preceding each
+ * occurence are consolidated.
+ */
+ protected function deduplicate($lines) {
+ $unique = array();
+ $comments = array();
+
+ foreach($lines as $line) {
+ if (strpos($line, '/*') === 0) {
+ $comments[] = $line;
+ continue;
+ }
+ if (!in_array($line, $unique)) {
+ $unique[] = $line;
+ }
+ array_splice($unique, array_search($line, $unique), 0, $comments);
+ $comments = array();
+ }
+ return array_merge($unique, $comments);
+ }
+
+ protected function sortProps($props, $split = false) {
+ $vars = array();
+ $imports = array();
+ $other = array();
+ $stack = array();
+
+ foreach ($props as $prop) {
+ switch ($prop[0]) {
+ case "comment":
+ $stack[] = $prop;
+ break;
+ case "assign":
+ $stack[] = $prop;
+ if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
+ $vars = array_merge($vars, $stack);
+ } else {
+ $other = array_merge($other, $stack);
+ }
+ $stack = array();
+ break;
+ case "import":
+ $id = self::$nextImportId++;
+ $prop[] = $id;
+ $stack[] = $prop;
+ $imports = array_merge($imports, $stack);
+ $other[] = array("import_mixin", $id);
+ $stack = array();
+ break;
+ default:
+ $stack[] = $prop;
+ $other = array_merge($other, $stack);
+ $stack = array();
+ break;
+ }
+ }
+ $other = array_merge($other, $stack);
+
+ if ($split) {
+ return array(array_merge($imports, $vars), $other);
+ } else {
+ return array_merge($imports, $vars, $other);
+ }
+ }
+
+ protected function compileMediaQuery($queries) {
+ $compiledQueries = array();
+ foreach ($queries as $query) {
+ $parts = array();
+ foreach ($query as $q) {
+ switch ($q[0]) {
+ case "mediaType":
+ $parts[] = implode(" ", array_slice($q, 1));
+ break;
+ case "mediaExp":
+ if (isset($q[2])) {
+ $parts[] = "($q[1]: " .
+ $this->compileValue($this->reduce($q[2])) . ")";
+ } else {
+ $parts[] = "($q[1])";
+ }
+ break;
+ case "variable":
+ $parts[] = $this->compileValue($this->reduce($q));
+ break;
+ }
+ }
+
+ if (count($parts) > 0) {
+ $compiledQueries[] = implode(" and ", $parts);
+ }
+ }
+
+ $out = "@media";
+ if (!empty($parts)) {
+ $out .= " " .
+ implode($this->formatter->selectorSeparator, $compiledQueries);
+ }
+ return $out;
+ }
+
+ protected function multiplyMedia($env, $childQueries = null) {
+ if (is_null($env) ||
+ !empty($env->block->type) && $env->block->type != "media")
+ {
+ return $childQueries;
+ }
+
+ // plain old block, skip
+ if (empty($env->block->type)) {
+ return $this->multiplyMedia($env->parent, $childQueries);
+ }
+
+ $out = array();
+ $queries = $env->block->queries;
+ if (is_null($childQueries)) {
+ $out = $queries;
+ } else {
+ foreach ($queries as $parent) {
+ foreach ($childQueries as $child) {
+ $out[] = array_merge($parent, $child);
+ }
+ }
+ }
+
+ return $this->multiplyMedia($env->parent, $out);
+ }
+
+ protected function expandParentSelectors(&$tag, $replace) {
+ $parts = explode("$&$", $tag);
+ $count = 0;
+ foreach ($parts as &$part) {
+ $part = str_replace($this->parentSelector, $replace, $part, $c);
+ $count += $c;
+ }
+ $tag = implode($this->parentSelector, $parts);
+ return $count;
+ }
+
+ protected function findClosestSelectors() {
+ $env = $this->env;
+ $selectors = null;
+ while ($env !== null) {
+ if (isset($env->selectors)) {
+ $selectors = $env->selectors;
+ break;
+ }
+ $env = $env->parent;
+ }
+
+ return $selectors;
+ }
+
+
+ // multiply $selectors against the nearest selectors in env
+ protected function multiplySelectors($selectors) {
+ // find parent selectors
+
+ $parentSelectors = $this->findClosestSelectors();
+ if (is_null($parentSelectors)) {
+ // kill parent reference in top level selector
+ foreach ($selectors as &$s) {
+ $this->expandParentSelectors($s, "");
+ }
+
+ return $selectors;
+ }
+
+ $out = array();
+ foreach ($parentSelectors as $parent) {
+ foreach ($selectors as $child) {
+ $count = $this->expandParentSelectors($child, $parent);
+
+ // don't prepend the parent tag if & was used
+ if ($count > 0) {
+ $out[] = trim($child);
+ } else {
+ $out[] = trim($parent . ' ' . $child);
+ }
+ }
+ }
+
+ return $out;
+ }
+
+ // reduces selector expressions
+ protected function compileSelectors($selectors) {
+ $out = array();
+
+ foreach ($selectors as $s) {
+ if (is_array($s)) {
+ list(, $value) = $s;
+ $out[] = trim($this->compileValue($this->reduce($value)));
+ } else {
+ $out[] = $s;
+ }
+ }
+
+ return $out;
+ }
+
+ protected function eq($left, $right) {
+ return $left == $right;
+ }
+
+ protected function patternMatch($block, $orderedArgs, $keywordArgs) {
+ // match the guards if it has them
+ // any one of the groups must have all its guards pass for a match
+ if (!empty($block->guards)) {
+ $groupPassed = false;
+ foreach ($block->guards as $guardGroup) {
+ foreach ($guardGroup as $guard) {
+ $this->pushEnv();
+ $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
+
+ $negate = false;
+ if ($guard[0] == "negate") {
+ $guard = $guard[1];
+ $negate = true;
+ }
+
+ $passed = $this->reduce($guard) == self::$TRUE;
+ if ($negate) $passed = !$passed;
+
+ $this->popEnv();
+
+ if ($passed) {
+ $groupPassed = true;
+ } else {
+ $groupPassed = false;
+ break;
+ }
+ }
+
+ if ($groupPassed) break;
+ }
+
+ if (!$groupPassed) {
+ return false;
+ }
+ }
+
+ if (empty($block->args)) {
+ return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
+ }
+
+ $remainingArgs = $block->args;
+ if ($keywordArgs) {
+ $remainingArgs = array();
+ foreach ($block->args as $arg) {
+ if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) {
+ continue;
+ }
+
+ $remainingArgs[] = $arg;
+ }
+ }
+
+ $i = -1; // no args
+ // try to match by arity or by argument literal
+ foreach ($remainingArgs as $i => $arg) {
+ switch ($arg[0]) {
+ case "lit":
+ if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
+ return false;
+ }
+ break;
+ case "arg":
+ // no arg and no default value
+ if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
+ return false;
+ }
+ break;
+ case "rest":
+ $i--; // rest can be empty
+ break 2;
+ }
+ }
+
+ if ($block->isVararg) {
+ return true; // not having enough is handled above
+ } else {
+ $numMatched = $i + 1;
+ // greater than becuase default values always match
+ return $numMatched >= count($orderedArgs);
+ }
+ }
+
+ protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) {
+ $matches = null;
+ foreach ($blocks as $block) {
+ // skip seen blocks that don't have arguments
+ if (isset($skip[$block->id]) && !isset($block->args)) {
+ continue;
+ }
+
+ if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
+ $matches[] = $block;
+ }
+ }
+
+ return $matches;
+ }
+
+ // attempt to find blocks matched by path and args
+ protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) {
+ if ($searchIn == null) return null;
+ if (isset($seen[$searchIn->id])) return null;
+ $seen[$searchIn->id] = true;
+
+ $name = $path[0];
+
+ if (isset($searchIn->children[$name])) {
+ $blocks = $searchIn->children[$name];
+ if (count($path) == 1) {
+ $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
+ if (!empty($matches)) {
+ // This will return all blocks that match in the closest
+ // scope that has any matching block, like lessjs
+ return $matches;
+ }
+ } else {
+ $matches = array();
+ foreach ($blocks as $subBlock) {
+ $subMatches = $this->findBlocks($subBlock,
+ array_slice($path, 1), $orderedArgs, $keywordArgs, $seen);
+
+ if (!is_null($subMatches)) {
+ foreach ($subMatches as $sm) {
+ $matches[] = $sm;
+ }
+ }
+ }
+
+ return count($matches) > 0 ? $matches : null;
+ }
+ }
+ if ($searchIn->parent === $searchIn) return null;
+ return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
+ }
+
+ // sets all argument names in $args to either the default value
+ // or the one passed in through $values
+ protected function zipSetArgs($args, $orderedValues, $keywordValues) {
+ $assignedValues = array();
+
+ $i = 0;
+ foreach ($args as $a) {
+ if ($a[0] == "arg") {
+ if (isset($keywordValues[$a[1]])) {
+ // has keyword arg
+ $value = $keywordValues[$a[1]];
+ } elseif (isset($orderedValues[$i])) {
+ // has ordered arg
+ $value = $orderedValues[$i];
+ $i++;
+ } elseif (isset($a[2])) {
+ // has default value
+ $value = $a[2];
+ } else {
+ $this->throwError("Failed to assign arg " . $a[1]);
+ $value = null; // :(
+ }
+
+ $value = $this->reduce($value);
+ $this->set($a[1], $value);
+ $assignedValues[] = $value;
+ } else {
+ // a lit
+ $i++;
+ }
+ }
+
+ // check for a rest
+ $last = end($args);
+ if ($last[0] == "rest") {
+ $rest = array_slice($orderedValues, count($args) - 1);
+ $this->set($last[1], $this->reduce(array("list", " ", $rest)));
+ }
+
+ // wow is this the only true use of PHP's + operator for arrays?
+ $this->env->arguments = $assignedValues + $orderedValues;
+ }
+
+ // compile a prop and update $lines or $blocks appropriately
+ protected function compileProp($prop, $block, $out) {
+ // set error position context
+ $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
+
+ switch ($prop[0]) {
+ case 'assign':
+ list(, $name, $value) = $prop;
+ if ($name[0] == $this->vPrefix) {
+ $this->set($name, $value);
+ } else {
+ $out->lines[] = $this->formatter->property($name,
+ $this->compileValue($this->reduce($value)));
+ }
+ break;
+ case 'block':
+ list(, $child) = $prop;
+ $this->compileBlock($child);
+ break;
+ case 'mixin':
+ list(, $path, $args, $suffix) = $prop;
+
+ $orderedArgs = array();
+ $keywordArgs = array();
+ foreach ((array)$args as $arg) {
+ $argval = null;
+ switch ($arg[0]) {
+ case "arg":
+ if (!isset($arg[2])) {
+ $orderedArgs[] = $this->reduce(array("variable", $arg[1]));
+ } else {
+ $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
+ }
+ break;
+
+ case "lit":
+ $orderedArgs[] = $this->reduce($arg[1]);
+ break;
+ default:
+ $this->throwError("Unknown arg type: " . $arg[0]);
+ }
+ }
+
+ $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
+
+ if ($mixins === null) {
+ $this->throwError("{$prop[1][0]} is undefined");
+ }
+
+ foreach ($mixins as $mixin) {
+ if ($mixin === $block && !$orderedArgs) {
+ continue;
+ }
+
+ $haveScope = false;
+ if (isset($mixin->parent->scope)) {
+ $haveScope = true;
+ $mixinParentEnv = $this->pushEnv();
+ $mixinParentEnv->storeParent = $mixin->parent->scope;
+ }
+
+ $haveArgs = false;
+ if (isset($mixin->args)) {
+ $haveArgs = true;
+ $this->pushEnv();
+ $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
+ }
+
+ $oldParent = $mixin->parent;
+ if ($mixin != $block) $mixin->parent = $block;
+
+ foreach ($this->sortProps($mixin->props) as $subProp) {
+ if ($suffix !== null &&
+ $subProp[0] == "assign" &&
+ is_string($subProp[1]) &&
+ $subProp[1]{0} != $this->vPrefix)
+ {
+ $subProp[2] = array(
+ 'list', ' ',
+ array($subProp[2], array('keyword', $suffix))
+ );
+ }
+
+ $this->compileProp($subProp, $mixin, $out);
+ }
+
+ $mixin->parent = $oldParent;
+
+ if ($haveArgs) $this->popEnv();
+ if ($haveScope) $this->popEnv();
+ }
+
+ break;
+ case 'raw':
+ $out->lines[] = $prop[1];
+ break;
+ case "directive":
+ list(, $name, $value) = $prop;
+ $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';';
+ break;
+ case "comment":
+ $out->lines[] = $prop[1];
+ break;
+ case "import";
+ list(, $importPath, $importId) = $prop;
+ $importPath = $this->reduce($importPath);
+
+ if (!isset($this->env->imports)) {
+ $this->env->imports = array();
+ }
+
+ $result = $this->tryImport($importPath, $block, $out);
+
+ $this->env->imports[$importId] = $result === false ?
+ array(false, "@import " . $this->compileValue($importPath).";") :
+ $result;
+
+ break;
+ case "import_mixin":
+ list(,$importId) = $prop;
+ $import = $this->env->imports[$importId];
+ if ($import[0] === false) {
+ if (isset($import[1])) {
+ $out->lines[] = $import[1];
+ }
+ } else {
+ list(, $bottom, $parser, $importDir) = $import;
+ $this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
+ }
+
+ break;
+ default:
+ $this->throwError("unknown op: {$prop[0]}\n");
+ }
+ }
+
+
+ /**
+ * Compiles a primitive value into a CSS property value.
+ *
+ * Values in lessphp are typed by being wrapped in arrays, their format is
+ * typically:
+ *
+ * array(type, contents [, additional_contents]*)
+ *
+ * The input is expected to be reduced. This function will not work on
+ * things like expressions and variables.
+ */
+ public function compileValue($value) {
+ switch ($value[0]) {
+ case 'list':
+ // [1] - delimiter
+ // [2] - array of values
+ return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
+ case 'raw_color':
+ if (!empty($this->formatter->compressColors)) {
+ return $this->compileValue($this->coerceColor($value));
+ }
+ return $value[1];
+ case 'keyword':
+ // [1] - the keyword
+ return $value[1];
+ case 'number':
+ list(, $num, $unit) = $value;
+ // [1] - the number
+ // [2] - the unit
+ if ($this->numberPrecision !== null) {
+ $num = round($num, $this->numberPrecision);
+ }
+ return $num . $unit;
+ case 'string':
+ // [1] - contents of string (includes quotes)
+ list(, $delim, $content) = $value;
+ foreach ($content as &$part) {
+ if (is_array($part)) {
+ $part = $this->compileValue($part);
+ }
+ }
+ return $delim . implode($content) . $delim;
+ case 'color':
+ // [1] - red component (either number or a %)
+ // [2] - green component
+ // [3] - blue component
+ // [4] - optional alpha component
+ list(, $r, $g, $b) = $value;
+ $r = round($r);
+ $g = round($g);
+ $b = round($b);
+
+ if (count($value) == 5 && $value[4] != 1) { // rgba
+ return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
+ }
+
+ $h = sprintf("#%02x%02x%02x", $r, $g, $b);
+
+ if (!empty($this->formatter->compressColors)) {
+ // Converting hex color to short notation (e.g. #003399 to #039)
+ if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
+ $h = '#' . $h[1] . $h[3] . $h[5];
+ }
+ }
+
+ return $h;
+
+ case 'function':
+ list(, $name, $args) = $value;
+ return $name.'('.$this->compileValue($args).')';
+ default: // assumed to be unit
+ $this->throwError("unknown value type: $value[0]");
+ }
+ }
+
+ protected function lib_pow($args) {
+ list($base, $exp) = $this->assertArgs($args, 2, "pow");
+ return pow($this->assertNumber($base), $this->assertNumber($exp));
+ }
+
+ protected function lib_pi() {
+ return pi();
+ }
+
+ protected function lib_mod($args) {
+ list($a, $b) = $this->assertArgs($args, 2, "mod");
+ return $this->assertNumber($a) % $this->assertNumber($b);
+ }
+
+ protected function lib_tan($num) {
+ return tan($this->assertNumber($num));
+ }
+
+ protected function lib_sin($num) {
+ return sin($this->assertNumber($num));
+ }
+
+ protected function lib_cos($num) {
+ return cos($this->assertNumber($num));
+ }
+
+ protected function lib_atan($num) {
+ $num = atan($this->assertNumber($num));
+ return array("number", $num, "rad");
+ }
+
+ protected function lib_asin($num) {
+ $num = asin($this->assertNumber($num));
+ return array("number", $num, "rad");
+ }
+
+ protected function lib_acos($num) {
+ $num = acos($this->assertNumber($num));
+ return array("number", $num, "rad");
+ }
+
+ protected function lib_sqrt($num) {
+ return sqrt($this->assertNumber($num));
+ }
+
+ protected function lib_extract($value) {
+ list($list, $idx) = $this->assertArgs($value, 2, "extract");
+ $idx = $this->assertNumber($idx);
+ // 1 indexed
+ if ($list[0] == "list" && isset($list[2][$idx - 1])) {
+ return $list[2][$idx - 1];
+ }
+ }
+
+ protected function lib_isnumber($value) {
+ return $this->toBool($value[0] == "number");
+ }
+
+ protected function lib_isstring($value) {
+ return $this->toBool($value[0] == "string");
+ }
+
+ protected function lib_iscolor($value) {
+ return $this->toBool($this->coerceColor($value));
+ }
+
+ protected function lib_iskeyword($value) {
+ return $this->toBool($value[0] == "keyword");
+ }
+
+ protected function lib_ispixel($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "px");
+ }
+
+ protected function lib_ispercentage($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "%");
+ }
+
+ protected function lib_isem($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "em");
+ }
+
+ protected function lib_isrem($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "rem");
+ }
+
+ protected function lib_rgbahex($color) {
+ $color = $this->coerceColor($color);
+ if (is_null($color))
+ $this->throwError("color expected for rgbahex");
+
+ return sprintf("#%02x%02x%02x%02x",
+ isset($color[4]) ? $color[4]*255 : 255,
+ $color[1],$color[2], $color[3]);
+ }
+
+ protected function lib_argb($color){
+ return $this->lib_rgbahex($color);
+ }
+
+ /**
+ * Given an url, decide whether to output a regular link or the base64-encoded contents of the file
+ *
+ * @param array $value either an argument list (two strings) or a single string
+ * @return string formatted url(), either as a link or base64-encoded
+ */
+ protected function lib_data_uri($value) {
+ $mime = ($value[0] === 'list') ? $value[2][0][2] : null;
+ $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0];
+
+ $fullpath = $this->findImport($url);
+
+ if($fullpath && ($fsize = filesize($fullpath)) !== false) {
+ // IE8 can't handle data uris larger than 32KB
+ if($fsize/1024 < 32) {
+ if(is_null($mime)) {
+ if(class_exists('finfo')) { // php 5.3+
+ // phpcs:ignore PHPCompatibility.PHP.NewClasses.finfoFound
+ $finfo = new finfo(FILEINFO_MIME);
+ $mime = explode('; ', $finfo->file($fullpath));
+ $mime = $mime[0];
+ } elseif(function_exists('mime_content_type')) { // PHP 5.2
+ $mime = mime_content_type($fullpath);
+ }
+ }
+
+ if(!is_null($mime)) // fallback if the mime type is still unknown
+ $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath)));
+ }
+ }
+
+ return 'url("'.$url.'")';
+ }
+
+ // utility func to unquote a string
+ protected function lib_e($arg) {
+ switch ($arg[0]) {
+ case "list":
+ $items = $arg[2];
+ if (isset($items[0])) {
+ return $this->lib_e($items[0]);
+ }
+ $this->throwError("unrecognised input");
+ case "string":
+ $arg[1] = "";
+ return $arg;
+ case "keyword":
+ return $arg;
+ default:
+ return array("keyword", $this->compileValue($arg));
+ }
+ }
+
+ protected function lib__sprintf($args) {
+ if ($args[0] != "list") return $args;
+ $values = $args[2];
+ $string = array_shift($values);
+ $template = $this->compileValue($this->lib_e($string));
+
+ $i = 0;
+ if (preg_match_all('/%[dsa]/', $template, $m)) {
+ foreach ($m[0] as $match) {
+ $val = isset($values[$i]) ?
+ $this->reduce($values[$i]) : array('keyword', '');
+
+ // lessjs compat, renders fully expanded color, not raw color
+ if ($color = $this->coerceColor($val)) {
+ $val = $color;
+ }
+
+ $i++;
+ $rep = $this->compileValue($this->lib_e($val));
+ $template = preg_replace('/'.self::preg_quote($match).'/',
+ $rep, $template, 1);
+ }
+ }
+
+ $d = $string[0] == "string" ? $string[1] : '"';
+ return array("string", $d, array($template));
+ }
+
+ protected function lib_floor($arg) {
+ $value = $this->assertNumber($arg);
+ return array("number", floor($value), $arg[2]);
+ }
+
+ protected function lib_ceil($arg) {
+ $value = $this->assertNumber($arg);
+ return array("number", ceil($value), $arg[2]);
+ }
+
+ protected function lib_round($arg) {
+ if($arg[0] != "list") {
+ $value = $this->assertNumber($arg);
+ return array("number", round($value), $arg[2]);
+ } else {
+ $value = $this->assertNumber($arg[2][0]);
+ $precision = $this->assertNumber($arg[2][1]);
+ return array("number", round($value, $precision), $arg[2][0][2]);
+ }
+ }
+
+ protected function lib_unit($arg) {
+ if ($arg[0] == "list") {
+ list($number, $newUnit) = $arg[2];
+ return array("number", $this->assertNumber($number),
+ $this->compileValue($this->lib_e($newUnit)));
+ } else {
+ return array("number", $this->assertNumber($arg), "");
+ }
+ }
+
+ /**
+ * Helper function to get arguments for color manipulation functions.
+ * takes a list that contains a color like thing and a percentage
+ */
+ public function colorArgs($args) {
+ if ($args[0] != 'list' || count($args[2]) < 2) {
+ return array(array('color', 0, 0, 0), 0);
+ }
+ list($color, $delta) = $args[2];
+ $color = $this->assertColor($color);
+ $delta = floatval($delta[1]);
+
+ return array($color, $delta);
+ }
+
+ protected function lib_darken($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_lighten($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_saturate($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_desaturate($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_spin($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+
+ $hsl[1] = $hsl[1] + $delta % 360;
+ if ($hsl[1] < 0) $hsl[1] += 360;
+
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_fadeout($args) {
+ list($color, $delta) = $this->colorArgs($args);
+ $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100);
+ return $color;
+ }
+
+ protected function lib_fadein($args) {
+ list($color, $delta) = $this->colorArgs($args);
+ $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100);
+ return $color;
+ }
+
+ protected function lib_hue($color) {
+ $hsl = $this->toHSL($this->assertColor($color));
+ return round($hsl[1]);
+ }
+
+ protected function lib_saturation($color) {
+ $hsl = $this->toHSL($this->assertColor($color));
+ return round($hsl[2]);
+ }
+
+ protected function lib_lightness($color) {
+ $hsl = $this->toHSL($this->assertColor($color));
+ return round($hsl[3]);
+ }
+
+ // get the alpha of a color
+ // defaults to 1 for non-colors or colors without an alpha
+ protected function lib_alpha($value) {
+ if (!is_null($color = $this->coerceColor($value))) {
+ return isset($color[4]) ? $color[4] : 1;
+ }
+ }
+
+ // set the alpha of the color
+ protected function lib_fade($args) {
+ list($color, $alpha) = $this->colorArgs($args);
+ $color[4] = $this->clamp($alpha / 100.0);
+ return $color;
+ }
+
+ protected function lib_percentage($arg) {
+ $num = $this->assertNumber($arg);
+ return array("number", $num*100, "%");
+ }
+
+ // mixes two colors by weight
+ // mix(@color1, @color2, [@weight: 50%]);
+ // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
+ protected function lib_mix($args) {
+ if ($args[0] != "list" || count($args[2]) < 2)
+ $this->throwError("mix expects (color1, color2, weight)");
+
+ list($first, $second) = $args[2];
+ $first = $this->assertColor($first);
+ $second = $this->assertColor($second);
+
+ $first_a = $this->lib_alpha($first);
+ $second_a = $this->lib_alpha($second);
+
+ if (isset($args[2][2])) {
+ $weight = $args[2][2][1] / 100.0;
+ } else {
+ $weight = 0.5;
+ }
+
+ $w = $weight * 2 - 1;
+ $a = $first_a - $second_a;
+
+ $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
+ $w2 = 1.0 - $w1;
+
+ $new = array('color',
+ $w1 * $first[1] + $w2 * $second[1],
+ $w1 * $first[2] + $w2 * $second[2],
+ $w1 * $first[3] + $w2 * $second[3],
+ );
+
+ if ($first_a != 1.0 || $second_a != 1.0) {
+ $new[] = $first_a * $weight + $second_a * ($weight - 1);
+ }
+
+ return $this->fixColor($new);
+ }
+
+ protected function lib_contrast($args) {
+ $darkColor = array('color', 0, 0, 0);
+ $lightColor = array('color', 255, 255, 255);
+ $threshold = 0.43;
+
+ if ( $args[0] == 'list' ) {
+ $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0]) : $lightColor;
+ $darkColor = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1]) : $darkColor;
+ $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2]) : $lightColor;
+ $threshold = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold;
+ }
+ else {
+ $inputColor = $this->assertColor($args);
+ }
+
+ $inputColor = $this->coerceColor($inputColor);
+ $darkColor = $this->coerceColor($darkColor);
+ $lightColor = $this->coerceColor($lightColor);
+
+ //Figure out which is actually light and dark!
+ if ( $this->lib_luma($darkColor) > $this->lib_luma($lightColor) ) {
+ $t = $lightColor;
+ $lightColor = $darkColor;
+ $darkColor = $t;
+ }
+
+ $inputColor_alpha = $this->lib_alpha($inputColor);
+ if ( ( $this->lib_luma($inputColor) * $inputColor_alpha) < $threshold) {
+ return $lightColor;
+ }
+ return $darkColor;
+ }
+
+ protected function lib_luma($color) {
+ $color = $this->coerceColor($color);
+ return (0.2126 * $color[0] / 255) + (0.7152 * $color[1] / 255) + (0.0722 * $color[2] / 255);
+ }
+
+
+ public function assertColor($value, $error = "expected color value") {
+ $color = $this->coerceColor($value);
+ if (is_null($color)) $this->throwError($error);
+ return $color;
+ }
+
+ public function assertNumber($value, $error = "expecting number") {
+ if ($value[0] == "number") return $value[1];
+ $this->throwError($error);
+ }
+
+ public function assertArgs($value, $expectedArgs, $name="") {
+ if ($expectedArgs == 1) {
+ return $value;
+ } else {
+ if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list");
+ $values = $value[2];
+ $numValues = count($values);
+ if ($expectedArgs != $numValues) {
+ if ($name) {
+ $name = $name . ": ";
+ }
+
+ $this->throwError("${name}expecting $expectedArgs arguments, got $numValues");
+ }
+
+ return $values;
+ }
+ }
+
+ protected function toHSL($color) {
+ if ($color[0] == 'hsl') return $color;
+
+ $r = $color[1] / 255;
+ $g = $color[2] / 255;
+ $b = $color[3] / 255;
+
+ $min = min($r, $g, $b);
+ $max = max($r, $g, $b);
+
+ $L = ($min + $max) / 2;
+ if ($min == $max) {
+ $S = $H = 0;
+ } else {
+ if ($L < 0.5)
+ $S = ($max - $min)/($max + $min);
+ else
+ $S = ($max - $min)/(2.0 - $max - $min);
+
+ if ($r == $max) $H = ($g - $b)/($max - $min);
+ elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min);
+ elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min);
+
+ }
+
+ $out = array('hsl',
+ ($H < 0 ? $H + 6 : $H)*60,
+ $S*100,
+ $L*100,
+ );
+
+ if (count($color) > 4) $out[] = $color[4]; // copy alpha
+ return $out;
+ }
+
+ protected function toRGB_helper($comp, $temp1, $temp2) {
+ if ($comp < 0) $comp += 1.0;
+ elseif ($comp > 1) $comp -= 1.0;
+
+ if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp;
+ if (2 * $comp < 1) return $temp2;
+ if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
+
+ return $temp1;
+ }
+
+ /**
+ * Converts a hsl array into a color value in rgb.
+ * Expects H to be in range of 0 to 360, S and L in 0 to 100
+ */
+ protected function toRGB($color) {
+ if ($color[0] == 'color') return $color;
+
+ $H = $color[1] / 360;
+ $S = $color[2] / 100;
+ $L = $color[3] / 100;
+
+ if ($S == 0) {
+ $r = $g = $b = $L;
+ } else {
+ $temp2 = $L < 0.5 ?
+ $L*(1.0 + $S) :
+ $L + $S - $L * $S;
+
+ $temp1 = 2.0 * $L - $temp2;
+
+ $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
+ $g = $this->toRGB_helper($H, $temp1, $temp2);
+ $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
+ }
+
+ // $out = array('color', round($r*255), round($g*255), round($b*255));
+ $out = array('color', $r*255, $g*255, $b*255);
+ if (count($color) > 4) $out[] = $color[4]; // copy alpha
+ return $out;
+ }
+
+ protected function clamp($v, $max = 1, $min = 0) {
+ return min($max, max($min, $v));
+ }
+
+ /**
+ * Convert the rgb, rgba, hsl color literals of function type
+ * as returned by the parser into values of color type.
+ */
+ protected function funcToColor($func) {
+ $fname = $func[1];
+ if ($func[2][0] != 'list') return false; // need a list of arguments
+ $rawComponents = $func[2][2];
+
+ if ($fname == 'hsl' || $fname == 'hsla') {
+ $hsl = array('hsl');
+ $i = 0;
+ foreach ($rawComponents as $c) {
+ $val = $this->reduce($c);
+ $val = isset($val[1]) ? floatval($val[1]) : 0;
+
+ if ($i == 0) $clamp = 360;
+ elseif ($i < 3) $clamp = 100;
+ else $clamp = 1;
+
+ $hsl[] = $this->clamp($val, $clamp);
+ $i++;
+ }
+
+ while (count($hsl) < 4) $hsl[] = 0;
+ return $this->toRGB($hsl);
+
+ } elseif ($fname == 'rgb' || $fname == 'rgba') {
+ $components = array();
+ $i = 1;
+ foreach ($rawComponents as $c) {
+ $c = $this->reduce($c);
+ if ($i < 4) {
+ if ($c[0] == "number" && $c[2] == "%") {
+ $components[] = 255 * ($c[1] / 100);
+ } else {
+ $components[] = floatval($c[1]);
+ }
+ } elseif ($i == 4) {
+ if ($c[0] == "number" && $c[2] == "%") {
+ $components[] = 1.0 * ($c[1] / 100);
+ } else {
+ $components[] = floatval($c[1]);
+ }
+ } else break;
+
+ $i++;
+ }
+ while (count($components) < 3) $components[] = 0;
+ array_unshift($components, 'color');
+ return $this->fixColor($components);
+ }
+
+ return false;
+ }
+
+ protected function reduce($value, $forExpression = false) {
+ switch ($value[0]) {
+ case "interpolate":
+ $reduced = $this->reduce($value[1]);
+ $var = $this->compileValue($reduced);
+ $res = $this->reduce(array("variable", $this->vPrefix . $var));
+
+ if ($res[0] == "raw_color") {
+ $res = $this->coerceColor($res);
+ }
+
+ if (empty($value[2])) $res = $this->lib_e($res);
+
+ return $res;
+ case "variable":
+ $key = $value[1];
+ if (is_array($key)) {
+ $key = $this->reduce($key);
+ $key = $this->vPrefix . $this->compileValue($this->lib_e($key));
+ }
+
+ $seen =& $this->env->seenNames;
+
+ if (!empty($seen[$key])) {
+ $this->throwError("infinite loop detected: $key");
+ }
+
+ $seen[$key] = true;
+ $out = $this->reduce($this->get($key));
+ $seen[$key] = false;
+ return $out;
+ case "list":
+ foreach ($value[2] as &$item) {
+ $item = $this->reduce($item, $forExpression);
+ }
+ return $value;
+ case "expression":
+ return $this->evaluate($value);
+ case "string":
+ foreach ($value[2] as &$part) {
+ if (is_array($part)) {
+ $strip = $part[0] == "variable";
+ $part = $this->reduce($part);
+ if ($strip) $part = $this->lib_e($part);
+ }
+ }
+ return $value;
+ case "escape":
+ list(,$inner) = $value;
+ return $this->lib_e($this->reduce($inner));
+ case "function":
+ $color = $this->funcToColor($value);
+ if ($color) return $color;
+
+ list(, $name, $args) = $value;
+ if ($name == "%") $name = "_sprintf";
+
+ $f = isset($this->libFunctions[$name]) ?
+ $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name));
+
+ if (is_callable($f)) {
+ if ($args[0] == 'list')
+ $args = self::compressList($args[2], $args[1]);
+
+ $ret = call_user_func($f, $this->reduce($args, true), $this);
+
+ if (is_null($ret)) {
+ return array("string", "", array(
+ $name, "(", $args, ")"
+ ));
+ }
+
+ // convert to a typed value if the result is a php primitive
+ if (is_numeric($ret)) $ret = array('number', $ret, "");
+ elseif (!is_array($ret)) $ret = array('keyword', $ret);
+
+ return $ret;
+ }
+
+ // plain function, reduce args
+ $value[2] = $this->reduce($value[2]);
+ return $value;
+ case "unary":
+ list(, $op, $exp) = $value;
+ $exp = $this->reduce($exp);
+
+ if ($exp[0] == "number") {
+ switch ($op) {
+ case "+":
+ return $exp;
+ case "-":
+ $exp[1] *= -1;
+ return $exp;
+ }
+ }
+ return array("string", "", array($op, $exp));
+ }
+
+ if ($forExpression) {
+ switch ($value[0]) {
+ case "keyword":
+ if ($color = $this->coerceColor($value)) {
+ return $color;
+ }
+ break;
+ case "raw_color":
+ return $this->coerceColor($value);
+ }
+ }
+
+ return $value;
+ }
+
+
+ // coerce a value for use in color operation
+ protected function coerceColor($value) {
+ switch($value[0]) {
+ case 'color': return $value;
+ case 'raw_color':
+ $c = array("color", 0, 0, 0);
+ $colorStr = substr($value[1], 1);
+ $num = hexdec($colorStr);
+ $width = strlen($colorStr) == 3 ? 16 : 256;
+
+ for ($i = 3; $i > 0; $i--) { // 3 2 1
+ $t = $num % $width;
+ $num /= $width;
+
+ $c[$i] = $t * (256/$width) + $t * floor(16/$width);
+ }
+
+ return $c;
+ case 'keyword':
+ $name = $value[1];
+ if (isset(self::$cssColors[$name])) {
+ $rgba = explode(',', self::$cssColors[$name]);
+
+ if(isset($rgba[3]))
+ return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
+
+ return array('color', $rgba[0], $rgba[1], $rgba[2]);
+ }
+ return null;
+ }
+ }
+
+ // make something string like into a string
+ protected function coerceString($value) {
+ switch ($value[0]) {
+ case "string":
+ return $value;
+ case "keyword":
+ return array("string", "", array($value[1]));
+ }
+ return null;
+ }
+
+ // turn list of length 1 into value type
+ protected function flattenList($value) {
+ if ($value[0] == "list" && count($value[2]) == 1) {
+ return $this->flattenList($value[2][0]);
+ }
+ return $value;
+ }
+
+ public function toBool($a) {
+ if ($a) return self::$TRUE;
+ else return self::$FALSE;
+ }
+
+ // evaluate an expression
+ protected function evaluate($exp) {
+ list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
+
+ $left = $this->reduce($left, true);
+ $right = $this->reduce($right, true);
+
+ if ($leftColor = $this->coerceColor($left)) {
+ $left = $leftColor;
+ }
+
+ if ($rightColor = $this->coerceColor($right)) {
+ $right = $rightColor;
+ }
+
+ $ltype = $left[0];
+ $rtype = $right[0];
+
+ // operators that work on all types
+ if ($op == "and") {
+ return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
+ }
+
+ if ($op == "=") {
+ return $this->toBool($this->eq($left, $right) );
+ }
+
+ if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) {
+ return $str;
+ }
+
+ // type based operators
+ $fname = "op_${ltype}_${rtype}";
+ if (is_callable(array($this, $fname))) {
+ $out = $this->$fname($op, $left, $right);
+ if (!is_null($out)) return $out;
+ }
+
+ // make the expression look it did before being parsed
+ $paddedOp = $op;
+ if ($whiteBefore) $paddedOp = " " . $paddedOp;
+ if ($whiteAfter) $paddedOp .= " ";
+
+ return array("string", "", array($left, $paddedOp, $right));
+ }
+
+ protected function stringConcatenate($left, $right) {
+ if ($strLeft = $this->coerceString($left)) {
+ if ($right[0] == "string") {
+ $right[1] = "";
+ }
+ $strLeft[2][] = $right;
+ return $strLeft;
+ }
+
+ if ($strRight = $this->coerceString($right)) {
+ array_unshift($strRight[2], $left);
+ return $strRight;
+ }
+ }
+
+
+ // make sure a color's components don't go out of bounds
+ protected function fixColor($c) {
+ foreach (range(1, 3) as $i) {
+ if ($c[$i] < 0) $c[$i] = 0;
+ if ($c[$i] > 255) $c[$i] = 255;
+ }
+
+ return $c;
+ }
+
+ protected function op_number_color($op, $lft, $rgt) {
+ if ($op == '+' || $op == '*') {
+ return $this->op_color_number($op, $rgt, $lft);
+ }
+ }
+
+ protected function op_color_number($op, $lft, $rgt) {
+ if ($rgt[0] == '%') $rgt[1] /= 100;
+
+ return $this->op_color_color($op, $lft,
+ array_fill(1, count($lft) - 1, $rgt[1]));
+ }
+
+ protected function op_color_color($op, $left, $right) {
+ $out = array('color');
+ $max = count($left) > count($right) ? count($left) : count($right);
+ foreach (range(1, $max - 1) as $i) {
+ $lval = isset($left[$i]) ? $left[$i] : 0;
+ $rval = isset($right[$i]) ? $right[$i] : 0;
+ switch ($op) {
+ case '+':
+ $out[] = $lval + $rval;
+ break;
+ case '-':
+ $out[] = $lval - $rval;
+ break;
+ case '*':
+ $out[] = $lval * $rval;
+ break;
+ case '%':
+ $out[] = $lval % $rval;
+ break;
+ case '/':
+ if ($rval == 0) $this->throwError("evaluate error: can't divide by zero");
+ $out[] = $lval / $rval;
+ break;
+ default:
+ $this->throwError('evaluate error: color op number failed on op '.$op);
+ }
+ }
+ return $this->fixColor($out);
+ }
+
+ function lib_red($color){
+ $color = $this->coerceColor($color);
+ if (is_null($color)) {
+ $this->throwError('color expected for red()');
+ }
+
+ return $color[1];
+ }
+
+ function lib_green($color){
+ $color = $this->coerceColor($color);
+ if (is_null($color)) {
+ $this->throwError('color expected for green()');
+ }
+
+ return $color[2];
+ }
+
+ function lib_blue($color){
+ $color = $this->coerceColor($color);
+ if (is_null($color)) {
+ $this->throwError('color expected for blue()');
+ }
+
+ return $color[3];
+ }
+
+
+ // operator on two numbers
+ protected function op_number_number($op, $left, $right) {
+ $unit = empty($left[2]) ? $right[2] : $left[2];
+
+ $value = 0;
+ switch ($op) {
+ case '+':
+ $value = $left[1] + $right[1];
+ break;
+ case '*':
+ $value = $left[1] * $right[1];
+ break;
+ case '-':
+ $value = $left[1] - $right[1];
+ break;
+ case '%':
+ $value = $left[1] % $right[1];
+ break;
+ case '/':
+ if ($right[1] == 0) $this->throwError('parse error: divide by zero');
+ $value = $left[1] / $right[1];
+ break;
+ case '<':
+ return $this->toBool($left[1] < $right[1]);
+ case '>':
+ return $this->toBool($left[1] > $right[1]);
+ case '>=':
+ return $this->toBool($left[1] >= $right[1]);
+ case '=<':
+ return $this->toBool($left[1] <= $right[1]);
+ default:
+ $this->throwError('parse error: unknown number operator: '.$op);
+ }
+
+ return array("number", $value, $unit);
+ }
+
+
+ /* environment functions */
+
+ protected function makeOutputBlock($type, $selectors = null) {
+ $b = new stdclass;
+ $b->lines = array();
+ $b->children = array();
+ $b->selectors = $selectors;
+ $b->type = $type;
+ $b->parent = $this->scope;
+ return $b;
+ }
+
+ // the state of execution
+ protected function pushEnv($block = null) {
+ $e = new stdclass;
+ $e->parent = $this->env;
+ $e->store = array();
+ $e->block = $block;
+
+ $this->env = $e;
+ return $e;
+ }
+
+ // pop something off the stack
+ protected function popEnv() {
+ $old = $this->env;
+ $this->env = $this->env->parent;
+ return $old;
+ }
+
+ // set something in the current env
+ protected function set($name, $value) {
+ $this->env->store[$name] = $value;
+ }
+
+
+ // get the highest occurrence entry for a name
+ protected function get($name) {
+ $current = $this->env;
+
+ $isArguments = $name == $this->vPrefix . 'arguments';
+ while ($current) {
+ if ($isArguments && isset($current->arguments)) {
+ return array('list', ' ', $current->arguments);
+ }
+
+ if (isset($current->store[$name]))
+ return $current->store[$name];
+ else {
+ $current = isset($current->storeParent) ?
+ $current->storeParent : $current->parent;
+ }
+ }
+
+ $this->throwError("variable $name is undefined");
+ }
+
+ // inject array of unparsed strings into environment as variables
+ protected function injectVariables($args) {
+ $this->pushEnv();
+ $parser = new lessc_parser($this, __METHOD__);
+ foreach ($args as $name => $strValue) {
+ if ($name{0} != '@') $name = '@'.$name;
+ $parser->count = 0;
+ $parser->buffer = (string)$strValue;
+ if (!$parser->propertyValue($value)) {
+ throw new Exception("failed to parse passed in variable $name: $strValue");
+ }
+
+ $this->set($name, $value);
+ }
+ }
+
+ /**
+ * Initialize any static state, can initialize parser for a file
+ * $opts isn't used yet
+ */
+ public function __construct($fname = null) {
+ if ($fname !== null) {
+ // used for deprecated parse method
+ $this->_parseFile = $fname;
+ }
+ }
+
+ public function compile($string, $name = null) {
+ $locale = setlocale(LC_NUMERIC, 0);
+ setlocale(LC_NUMERIC, "C");
+
+ $this->parser = $this->makeParser($name);
+ $root = $this->parser->parse($string);
+
+ $this->env = null;
+ $this->scope = null;
+
+ $this->formatter = $this->newFormatter();
+
+ if (!empty($this->registeredVars)) {
+ $this->injectVariables($this->registeredVars);
+ }
+
+ $this->sourceParser = $this->parser; // used for error messages
+ $this->compileBlock($root);
+
+ ob_start();
+ $this->formatter->block($this->scope);
+ $out = ob_get_clean();
+ setlocale(LC_NUMERIC, $locale);
+ return $out;
+ }
+
+ public function compileFile($fname, $outFname = null) {
+ if (!is_readable($fname)) {
+ throw new Exception('load error: failed to find '.$fname);
+ }
+
+ $pi = pathinfo($fname);
+
+ $oldImport = $this->importDir;
+
+ $this->importDir = (array)$this->importDir;
+ $this->importDir[] = $pi['dirname'].'/';
+
+ $this->addParsedFile($fname);
+
+ $out = $this->compile(file_get_contents($fname), $fname);
+
+ $this->importDir = $oldImport;
+
+ if ($outFname !== null) {
+ return file_put_contents($outFname, $out);
+ }
+
+ return $out;
+ }
+
+ // compile only if changed input has changed or output doesn't exist
+ public function checkedCompile($in, $out) {
+ if (!is_file($out) || filemtime($in) > filemtime($out)) {
+ $this->compileFile($in, $out);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Execute lessphp on a .less file or a lessphp cache structure
+ *
+ * The lessphp cache structure contains information about a specific
+ * less file having been parsed. It can be used as a hint for future
+ * calls to determine whether or not a rebuild is required.
+ *
+ * The cache structure contains two important keys that may be used
+ * externally:
+ *
+ * compiled: The final compiled CSS
+ * updated: The time (in seconds) the CSS was last compiled
+ *
+ * The cache structure is a plain-ol' PHP associative array and can
+ * be serialized and unserialized without a hitch.
+ *
+ * @param mixed $in Input
+ * @param bool $force Force rebuild?
+ * @return array lessphp cache structure
+ */
+ public function cachedCompile($in, $force = false) {
+ // assume no root
+ $root = null;
+
+ if (is_string($in)) {
+ $root = $in;
+ } elseif (is_array($in) and isset($in['root'])) {
+ if ($force or ! isset($in['files'])) {
+ // If we are forcing a recompile or if for some reason the
+ // structure does not contain any file information we should
+ // specify the root to trigger a rebuild.
+ $root = $in['root'];
+ } elseif (isset($in['files']) and is_array($in['files'])) {
+ foreach ($in['files'] as $fname => $ftime ) {
+ if (!file_exists($fname) or filemtime($fname) > $ftime) {
+ // One of the files we knew about previously has changed
+ // so we should look at our incoming root again.
+ $root = $in['root'];
+ break;
+ }
+ }
+ }
+ } else {
+ // TODO: Throw an exception? We got neither a string nor something
+ // that looks like a compatible lessphp cache structure.
+ return null;
+ }
+
+ if ($root !== null) {
+ // If we have a root value which means we should rebuild.
+ $out = array();
+ $out['root'] = $root;
+ $out['compiled'] = $this->compileFile($root);
+ $out['files'] = $this->allParsedFiles();
+ $out['updated'] = time();
+ return $out;
+ } else {
+ // No changes, pass back the structure
+ // we were given initially.
+ return $in;
+ }
+
+ }
+
+ // parse and compile buffer
+ // This is deprecated
+ public function parse($str = null, $initialVariables = null) {
+ if (is_array($str)) {
+ $initialVariables = $str;
+ $str = null;
+ }
+
+ $oldVars = $this->registeredVars;
+ if ($initialVariables !== null) {
+ $this->setVariables($initialVariables);
+ }
+
+ if ($str == null) {
+ if (empty($this->_parseFile)) {
+ throw new exception("nothing to parse");
+ }
+
+ $out = $this->compileFile($this->_parseFile);
+ } else {
+ $out = $this->compile($str);
+ }
+
+ $this->registeredVars = $oldVars;
+ return $out;
+ }
+
+ protected function makeParser($name) {
+ $parser = new lessc_parser($this, $name);
+ $parser->writeComments = $this->preserveComments;
+
+ return $parser;
+ }
+
+ public function setFormatter($name) {
+ $this->formatterName = $name;
+ }
+
+ protected function newFormatter() {
+ $className = "lessc_formatter_lessjs";
+ if (!empty($this->formatterName)) {
+ if (!is_string($this->formatterName))
+ return $this->formatterName;
+ $className = "lessc_formatter_$this->formatterName";
+ }
+
+ return new $className;
+ }
+
+ public function setPreserveComments($preserve) {
+ $this->preserveComments = $preserve;
+ }
+
+ public function registerFunction($name, $func) {
+ $this->libFunctions[$name] = $func;
+ }
+
+ public function unregisterFunction($name) {
+ unset($this->libFunctions[$name]);
+ }
+
+ public function setVariables($variables) {
+ $this->registeredVars = array_merge($this->registeredVars, $variables);
+ }
+
+ public function unsetVariable($name) {
+ unset($this->registeredVars[$name]);
+ }
+
+ public function setImportDir($dirs) {
+ $this->importDir = (array)$dirs;
+ }
+
+ public function addImportDir($dir) {
+ $this->importDir = (array)$this->importDir;
+ $this->importDir[] = $dir;
+ }
+
+ public function allParsedFiles() {
+ return $this->allParsedFiles;
+ }
+
+ public function addParsedFile($file) {
+ $this->allParsedFiles[realpath($file)] = filemtime($file);
+ }
+
+ /**
+ * Uses the current value of $this->count to show line and line number
+ */
+ public function throwError($msg = null) {
+ if ($this->sourceLoc >= 0) {
+ $this->sourceParser->throwError($msg, $this->sourceLoc);
+ }
+ throw new exception($msg);
+ }
+
+ // compile file $in to file $out if $in is newer than $out
+ // returns true when it compiles, false otherwise
+ public static function ccompile($in, $out, $less = null) {
+ if ($less === null) {
+ $less = new self;
+ }
+ return $less->checkedCompile($in, $out);
+ }
+
+ public static function cexecute($in, $force = false, $less = null) {
+ if ($less === null) {
+ $less = new self;
+ }
+ return $less->cachedCompile($in, $force);
+ }
+
+ static protected $cssColors = array(
+ 'aliceblue' => '240,248,255',
+ 'antiquewhite' => '250,235,215',
+ 'aqua' => '0,255,255',
+ 'aquamarine' => '127,255,212',
+ 'azure' => '240,255,255',
+ 'beige' => '245,245,220',
+ 'bisque' => '255,228,196',
+ 'black' => '0,0,0',
+ 'blanchedalmond' => '255,235,205',
+ 'blue' => '0,0,255',
+ 'blueviolet' => '138,43,226',
+ 'brown' => '165,42,42',
+ 'burlywood' => '222,184,135',
+ 'cadetblue' => '95,158,160',
+ 'chartreuse' => '127,255,0',
+ 'chocolate' => '210,105,30',
+ 'coral' => '255,127,80',
+ 'cornflowerblue' => '100,149,237',
+ 'cornsilk' => '255,248,220',
+ 'crimson' => '220,20,60',
+ 'cyan' => '0,255,255',
+ 'darkblue' => '0,0,139',
+ 'darkcyan' => '0,139,139',
+ 'darkgoldenrod' => '184,134,11',
+ 'darkgray' => '169,169,169',
+ 'darkgreen' => '0,100,0',
+ 'darkgrey' => '169,169,169',
+ 'darkkhaki' => '189,183,107',
+ 'darkmagenta' => '139,0,139',
+ 'darkolivegreen' => '85,107,47',
+ 'darkorange' => '255,140,0',
+ 'darkorchid' => '153,50,204',
+ 'darkred' => '139,0,0',
+ 'darksalmon' => '233,150,122',
+ 'darkseagreen' => '143,188,143',
+ 'darkslateblue' => '72,61,139',
+ 'darkslategray' => '47,79,79',
+ 'darkslategrey' => '47,79,79',
+ 'darkturquoise' => '0,206,209',
+ 'darkviolet' => '148,0,211',
+ 'deeppink' => '255,20,147',
+ 'deepskyblue' => '0,191,255',
+ 'dimgray' => '105,105,105',
+ 'dimgrey' => '105,105,105',
+ 'dodgerblue' => '30,144,255',
+ 'firebrick' => '178,34,34',
+ 'floralwhite' => '255,250,240',
+ 'forestgreen' => '34,139,34',
+ 'fuchsia' => '255,0,255',
+ 'gainsboro' => '220,220,220',
+ 'ghostwhite' => '248,248,255',
+ 'gold' => '255,215,0',
+ 'goldenrod' => '218,165,32',
+ 'gray' => '128,128,128',
+ 'green' => '0,128,0',
+ 'greenyellow' => '173,255,47',
+ 'grey' => '128,128,128',
+ 'honeydew' => '240,255,240',
+ 'hotpink' => '255,105,180',
+ 'indianred' => '205,92,92',
+ 'indigo' => '75,0,130',
+ 'ivory' => '255,255,240',
+ 'khaki' => '240,230,140',
+ 'lavender' => '230,230,250',
+ 'lavenderblush' => '255,240,245',
+ 'lawngreen' => '124,252,0',
+ 'lemonchiffon' => '255,250,205',
+ 'lightblue' => '173,216,230',
+ 'lightcoral' => '240,128,128',
+ 'lightcyan' => '224,255,255',
+ 'lightgoldenrodyellow' => '250,250,210',
+ 'lightgray' => '211,211,211',
+ 'lightgreen' => '144,238,144',
+ 'lightgrey' => '211,211,211',
+ 'lightpink' => '255,182,193',
+ 'lightsalmon' => '255,160,122',
+ 'lightseagreen' => '32,178,170',
+ 'lightskyblue' => '135,206,250',
+ 'lightslategray' => '119,136,153',
+ 'lightslategrey' => '119,136,153',
+ 'lightsteelblue' => '176,196,222',
+ 'lightyellow' => '255,255,224',
+ 'lime' => '0,255,0',
+ 'limegreen' => '50,205,50',
+ 'linen' => '250,240,230',
+ 'magenta' => '255,0,255',
+ 'maroon' => '128,0,0',
+ 'mediumaquamarine' => '102,205,170',
+ 'mediumblue' => '0,0,205',
+ 'mediumorchid' => '186,85,211',
+ 'mediumpurple' => '147,112,219',
+ 'mediumseagreen' => '60,179,113',
+ 'mediumslateblue' => '123,104,238',
+ 'mediumspringgreen' => '0,250,154',
+ 'mediumturquoise' => '72,209,204',
+ 'mediumvioletred' => '199,21,133',
+ 'midnightblue' => '25,25,112',
+ 'mintcream' => '245,255,250',
+ 'mistyrose' => '255,228,225',
+ 'moccasin' => '255,228,181',
+ 'navajowhite' => '255,222,173',
+ 'navy' => '0,0,128',
+ 'oldlace' => '253,245,230',
+ 'olive' => '128,128,0',
+ 'olivedrab' => '107,142,35',
+ 'orange' => '255,165,0',
+ 'orangered' => '255,69,0',
+ 'orchid' => '218,112,214',
+ 'palegoldenrod' => '238,232,170',
+ 'palegreen' => '152,251,152',
+ 'paleturquoise' => '175,238,238',
+ 'palevioletred' => '219,112,147',
+ 'papayawhip' => '255,239,213',
+ 'peachpuff' => '255,218,185',
+ 'peru' => '205,133,63',
+ 'pink' => '255,192,203',
+ 'plum' => '221,160,221',
+ 'powderblue' => '176,224,230',
+ 'purple' => '128,0,128',
+ 'red' => '255,0,0',
+ 'rosybrown' => '188,143,143',
+ 'royalblue' => '65,105,225',
+ 'saddlebrown' => '139,69,19',
+ 'salmon' => '250,128,114',
+ 'sandybrown' => '244,164,96',
+ 'seagreen' => '46,139,87',
+ 'seashell' => '255,245,238',
+ 'sienna' => '160,82,45',
+ 'silver' => '192,192,192',
+ 'skyblue' => '135,206,235',
+ 'slateblue' => '106,90,205',
+ 'slategray' => '112,128,144',
+ 'slategrey' => '112,128,144',
+ 'snow' => '255,250,250',
+ 'springgreen' => '0,255,127',
+ 'steelblue' => '70,130,180',
+ 'tan' => '210,180,140',
+ 'teal' => '0,128,128',
+ 'thistle' => '216,191,216',
+ 'tomato' => '255,99,71',
+ 'transparent' => '0,0,0,0',
+ 'turquoise' => '64,224,208',
+ 'violet' => '238,130,238',
+ 'wheat' => '245,222,179',
+ 'white' => '255,255,255',
+ 'whitesmoke' => '245,245,245',
+ 'yellow' => '255,255,0',
+ 'yellowgreen' => '154,205,50'
+ );
+}
+
+// responsible for taking a string of LESS code and converting it into a
+// syntax tree
+class lessc_parser {
+ static protected $nextBlockId = 0; // used to uniquely identify blocks
+
+ static protected $precedence = array(
+ '=<' => 0,
+ '>=' => 0,
+ '=' => 0,
+ '<' => 0,
+ '>' => 0,
+
+ '+' => 1,
+ '-' => 1,
+ '*' => 2,
+ '/' => 2,
+ '%' => 2,
+ );
+
+ static protected $whitePattern;
+ static protected $commentMulti;
+
+ static protected $commentSingle = "//";
+ static protected $commentMultiLeft = "/*";
+ static protected $commentMultiRight = "*/";
+
+ // regex string to match any of the operators
+ static protected $operatorString;
+
+ // these properties will supress division unless it's inside parenthases
+ static protected $supressDivisionProps =
+ array('/border-radius$/i', '/^font$/i');
+
+ protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport");
+ protected $lineDirectives = array("charset");
+
+ /**
+ * if we are in parens we can be more liberal with whitespace around
+ * operators because it must evaluate to a single value and thus is less
+ * ambiguous.
+ *
+ * Consider:
+ * property1: 10 -5; // is two numbers, 10 and -5
+ * property2: (10 -5); // should evaluate to 5
+ */
+ protected $inParens = false;
+
+ // caches preg escaped literals
+ static protected $literalCache = array();
+
+ public function __construct($lessc, $sourceName = null) {
+ $this->eatWhiteDefault = true;
+ // reference to less needed for vPrefix, mPrefix, and parentSelector
+ $this->lessc = $lessc;
+
+ $this->sourceName = $sourceName; // name used for error messages
+
+ $this->writeComments = false;
+
+ if (!self::$operatorString) {
+ self::$operatorString =
+ '('.implode('|', array_map(array('lessc', 'preg_quote'),
+ array_keys(self::$precedence))).')';
+
+ $commentSingle = lessc::preg_quote(self::$commentSingle);
+ $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft);
+ $commentMultiRight = lessc::preg_quote(self::$commentMultiRight);
+
+ self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
+ self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
+ }
+ }
+
+ public function parse($buffer) {
+ $this->count = 0;
+ $this->line = 1;
+
+ $this->env = null; // block stack
+ $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
+ $this->pushSpecialBlock("root");
+ $this->eatWhiteDefault = true;
+ $this->seenComments = array();
+
+ // trim whitespace on head
+ // if (preg_match('/^\s+/', $this->buffer, $m)) {
+ // $this->line += substr_count($m[0], "\n");
+ // $this->buffer = ltrim($this->buffer);
+ // }
+ $this->whitespace();
+
+ // parse the entire file
+ while (false !== $this->parseChunk());
+
+ if ($this->count != strlen($this->buffer))
+ $this->throwError();
+
+ // TODO report where the block was opened
+ if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) )
+ throw new exception('parse error: unclosed block');
+
+ return $this->env;
+ }
+
+ /**
+ * Parse a single chunk off the head of the buffer and append it to the
+ * current parse environment.
+ * Returns false when the buffer is empty, or when there is an error.
+ *
+ * This function is called repeatedly until the entire document is
+ * parsed.
+ *
+ * This parser is most similar to a recursive descent parser. Single
+ * functions represent discrete grammatical rules for the language, and
+ * they are able to capture the text that represents those rules.
+ *
+ * Consider the function lessc::keyword(). (all parse functions are
+ * structured the same)
+ *
+ * The function takes a single reference argument. When calling the
+ * function it will attempt to match a keyword on the head of the buffer.
+ * If it is successful, it will place the keyword in the referenced
+ * argument, advance the position in the buffer, and return true. If it
+ * fails then it won't advance the buffer and it will return false.
+ *
+ * All of these parse functions are powered by lessc::match(), which behaves
+ * the same way, but takes a literal regular expression. Sometimes it is
+ * more convenient to use match instead of creating a new function.
+ *
+ * Because of the format of the functions, to parse an entire string of
+ * grammatical rules, you can chain them together using &&.
+ *
+ * But, if some of the rules in the chain succeed before one fails, then
+ * the buffer position will be left at an invalid state. In order to
+ * avoid this, lessc::seek() is used to remember and set buffer positions.
+ *
+ * Before parsing a chain, use $s = $this->seek() to remember the current
+ * position into $s. Then if a chain fails, use $this->seek($s) to
+ * go back where we started.
+ */
+ protected function parseChunk() {
+ if (empty($this->buffer)) return false;
+ $s = $this->seek();
+
+ if ($this->whitespace()) {
+ return true;
+ }
+
+ // setting a property
+ if ($this->keyword($key) && $this->assign() &&
+ $this->propertyValue($value, $key) && $this->end())
+ {
+ $this->append(array('assign', $key, $value), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+
+ // look for special css blocks
+ if ($this->literal('@', false)) {
+ $this->count--;
+
+ // media
+ if ($this->literal('@media')) {
+ if (($this->mediaQueryList($mediaQueries) || true)
+ && $this->literal('{'))
+ {
+ $media = $this->pushSpecialBlock("media");
+ $media->queries = is_null($mediaQueries) ? array() : $mediaQueries;
+ return true;
+ } else {
+ $this->seek($s);
+ return false;
+ }
+ }
+
+ if ($this->literal("@", false) && $this->keyword($dirName)) {
+ if ($this->isDirective($dirName, $this->blockDirectives)) {
+ if (($this->openString("{", $dirValue, null, array(";")) || true) &&
+ $this->literal("{"))
+ {
+ $dir = $this->pushSpecialBlock("directive");
+ $dir->name = $dirName;
+ if (isset($dirValue)) $dir->value = $dirValue;
+ return true;
+ }
+ } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
+ if ($this->propertyValue($dirValue) && $this->end()) {
+ $this->append(array("directive", $dirName, $dirValue));
+ return true;
+ }
+ }
+ }
+
+ $this->seek($s);
+ }
+
+ // setting a variable
+ if ($this->variable($var) && $this->assign() &&
+ $this->propertyValue($value) && $this->end())
+ {
+ $this->append(array('assign', $var, $value), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->import($importValue)) {
+ $this->append($importValue, $s);
+ return true;
+ }
+
+ // opening parametric mixin
+ if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
+ ($this->guards($guards) || true) &&
+ $this->literal('{'))
+ {
+ $block = $this->pushBlock($this->fixTags(array($tag)));
+ $block->args = $args;
+ $block->isVararg = $isVararg;
+ if (!empty($guards)) $block->guards = $guards;
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // opening a simple block
+ if ($this->tags($tags) && $this->literal('{', false)) {
+ $tags = $this->fixTags($tags);
+ $this->pushBlock($tags);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // closing a block
+ if ($this->literal('}', false)) {
+ try {
+ $block = $this->pop();
+ } catch (exception $e) {
+ $this->seek($s);
+ $this->throwError($e->getMessage());
+ }
+
+ $hidden = false;
+ if (is_null($block->type)) {
+ $hidden = true;
+ if (!isset($block->args)) {
+ foreach ($block->tags as $tag) {
+ if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) {
+ $hidden = false;
+ break;
+ }
+ }
+ }
+
+ foreach ($block->tags as $tag) {
+ if (is_string($tag)) {
+ $this->env->children[$tag][] = $block;
+ }
+ }
+ }
+
+ if (!$hidden) {
+ $this->append(array('block', $block), $s);
+ }
+
+ // this is done here so comments aren't bundled into he block that
+ // was just closed
+ $this->whitespace();
+ return true;
+ }
+
+ // mixin
+ if ($this->mixinTags($tags) &&
+ ($this->argumentDef($argv, $isVararg) || true) &&
+ ($this->keyword($suffix) || true) && $this->end())
+ {
+ $tags = $this->fixTags($tags);
+ $this->append(array('mixin', $tags, $argv, $suffix), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // spare ;
+ if ($this->literal(';')) return true;
+
+ return false; // got nothing, throw error
+ }
+
+ protected function isDirective($dirname, $directives) {
+ // TODO: cache pattern in parser
+ $pattern = implode("|",
+ array_map(array("lessc", "preg_quote"), $directives));
+ $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
+
+ return preg_match($pattern, $dirname);
+ }
+
+ protected function fixTags($tags) {
+ // move @ tags out of variable namespace
+ foreach ($tags as &$tag) {
+ if ($tag{0} == $this->lessc->vPrefix)
+ $tag[0] = $this->lessc->mPrefix;
+ }
+ return $tags;
+ }
+
+ // a list of expressions
+ protected function expressionList(&$exps) {
+ $values = array();
+
+ while ($this->expression($exp)) {
+ $values[] = $exp;
+ }
+
+ if (count($values) == 0) return false;
+
+ $exps = lessc::compressList($values, ' ');
+ return true;
+ }
+
+ /**
+ * Attempt to consume an expression.
+ * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
+ */
+ protected function expression(&$out) {
+ if ($this->value($lhs)) {
+ $out = $this->expHelper($lhs, 0);
+
+ // look for / shorthand
+ if (!empty($this->env->supressedDivision)) {
+ unset($this->env->supressedDivision);
+ $s = $this->seek();
+ if ($this->literal("/") && $this->value($rhs)) {
+ $out = array("list", "",
+ array($out, array("keyword", "/"), $rhs));
+ } else {
+ $this->seek($s);
+ }
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * recursively parse infix equation with $lhs at precedence $minP
+ */
+ protected function expHelper($lhs, $minP) {
+ $this->inExp = true;
+ $ss = $this->seek();
+
+ while (true) {
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+
+ // If there is whitespace before the operator, then we require
+ // whitespace after the operator for it to be an expression
+ $needWhite = $whiteBefore && !$this->inParens;
+
+ if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
+ if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) {
+ foreach (self::$supressDivisionProps as $pattern) {
+ if (preg_match($pattern, $this->env->currentProperty)) {
+ $this->env->supressedDivision = true;
+ break 2;
+ }
+ }
+ }
+
+
+ $whiteAfter = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+
+ if (!$this->value($rhs)) break;
+
+ // peek for next operator to see what to do with rhs
+ if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) {
+ $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
+ }
+
+ $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter);
+ $ss = $this->seek();
+
+ continue;
+ }
+
+ break;
+ }
+
+ $this->seek($ss);
+
+ return $lhs;
+ }
+
+ // consume a list of values for a property
+ public function propertyValue(&$value, $keyName = null) {
+ $values = array();
+
+ if ($keyName !== null) $this->env->currentProperty = $keyName;
+
+ $s = null;
+ while ($this->expressionList($v)) {
+ $values[] = $v;
+ $s = $this->seek();
+ if (!$this->literal(',')) break;
+ }
+
+ if ($s) $this->seek($s);
+
+ if ($keyName !== null) unset($this->env->currentProperty);
+
+ if (count($values) == 0) return false;
+
+ $value = lessc::compressList($values, ', ');
+ return true;
+ }
+
+ protected function parenValue(&$out) {
+ $s = $this->seek();
+
+ // speed shortcut
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") {
+ return false;
+ }
+
+ $inParens = $this->inParens;
+ if ($this->literal("(") &&
+ ($this->inParens = true) && $this->expression($exp) &&
+ $this->literal(")"))
+ {
+ $out = $exp;
+ $this->inParens = $inParens;
+ return true;
+ } else {
+ $this->inParens = $inParens;
+ $this->seek($s);
+ }
+
+ return false;
+ }
+
+ // a single value
+ protected function value(&$value) {
+ $s = $this->seek();
+
+ // speed shortcut
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") {
+ // negation
+ if ($this->literal("-", false) &&
+ (($this->variable($inner) && $inner = array("variable", $inner)) ||
+ $this->unit($inner) ||
+ $this->parenValue($inner)))
+ {
+ $value = array("unary", "-", $inner);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+ }
+
+ if ($this->parenValue($value)) return true;
+ if ($this->unit($value)) return true;
+ if ($this->color($value)) return true;
+ if ($this->func($value)) return true;
+ if ($this->string($value)) return true;
+
+ if ($this->keyword($word)) {
+ $value = array('keyword', $word);
+ return true;
+ }
+
+ // try a variable
+ if ($this->variable($var)) {
+ $value = array('variable', $var);
+ return true;
+ }
+
+ // unquote string (should this work on any type?
+ if ($this->literal("~") && $this->string($str)) {
+ $value = array("escape", $str);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // css hack: \0
+ if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
+ $value = array('keyword', '\\'.$m[1]);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ return false;
+ }
+
+ // an import statement
+ protected function import(&$out) {
+ if (!$this->literal('@import')) return false;
+
+ // @import "something.css" media;
+ // @import url("something.css") media;
+ // @import url(something.css) media;
+
+ if ($this->propertyValue($value)) {
+ $out = array("import", $value);
+ return true;
+ }
+ }
+
+ protected function mediaQueryList(&$out) {
+ if ($this->genericList($list, "mediaQuery", ",", false)) {
+ $out = $list[2];
+ return true;
+ }
+ return false;
+ }
+
+ protected function mediaQuery(&$out) {
+ $s = $this->seek();
+
+ $expressions = null;
+ $parts = array();
+
+ if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) {
+ $prop = array("mediaType");
+ if (isset($only)) $prop[] = "only";
+ if (isset($not)) $prop[] = "not";
+ $prop[] = $mediaType;
+ $parts[] = $prop;
+ } else {
+ $this->seek($s);
+ }
+
+
+ if (!empty($mediaType) && !$this->literal("and")) {
+ // ~
+ } else {
+ $this->genericList($expressions, "mediaExpression", "and", false);
+ if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
+ }
+
+ if (count($parts) == 0) {
+ $this->seek($s);
+ return false;
+ }
+
+ $out = $parts;
+ return true;
+ }
+
+ protected function mediaExpression(&$out) {
+ $s = $this->seek();
+ $value = null;
+ if ($this->literal("(") &&
+ $this->keyword($feature) &&
+ ($this->literal(":") && $this->expression($value) || true) &&
+ $this->literal(")"))
+ {
+ $out = array("mediaExp", $feature);
+ if ($value) $out[] = $value;
+ return true;
+ } elseif ($this->variable($variable)) {
+ $out = array('variable', $variable);
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ // an unbounded string stopped by $end
+ protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ $stop = array("'", '"', "@{", $end);
+ $stop = array_map(array("lessc", "preg_quote"), $stop);
+ // $stop[] = self::$commentMulti;
+
+ if (!is_null($rejectStrs)) {
+ $stop = array_merge($stop, $rejectStrs);
+ }
+
+ $patt = '(.*?)('.implode("|", $stop).')';
+
+ $nestingLevel = 0;
+
+ $content = array();
+ while ($this->match($patt, $m, false)) {
+ if (!empty($m[1])) {
+ $content[] = $m[1];
+ if ($nestingOpen) {
+ $nestingLevel += substr_count($m[1], $nestingOpen);
+ }
+ }
+
+ $tok = $m[2];
+
+ $this->count-= strlen($tok);
+ if ($tok == $end) {
+ if ($nestingLevel == 0) {
+ break;
+ } else {
+ $nestingLevel--;
+ }
+ }
+
+ if (($tok == "'" || $tok == '"') && $this->string($str)) {
+ $content[] = $str;
+ continue;
+ }
+
+ if ($tok == "@{" && $this->interpolation($inter)) {
+ $content[] = $inter;
+ continue;
+ }
+
+ if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
+ break;
+ }
+
+ $content[] = $tok;
+ $this->count+= strlen($tok);
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if (count($content) == 0) return false;
+
+ // trim the end
+ if (is_string(end($content))) {
+ $content[count($content) - 1] = rtrim(end($content));
+ }
+
+ $out = array("string", "", $content);
+ return true;
+ }
+
+ protected function string(&$out) {
+ $s = $this->seek();
+ if ($this->literal('"', false)) {
+ $delim = '"';
+ } elseif ($this->literal("'", false)) {
+ $delim = "'";
+ } else {
+ return false;
+ }
+
+ $content = array();
+
+ // look for either ending delim , escape, or string interpolation
+ $patt = '([^\n]*?)(@\{|\\\\|' .
+ lessc::preg_quote($delim).')';
+
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while ($this->match($patt, $m, false)) {
+ $content[] = $m[1];
+ if ($m[2] == "@{") {
+ $this->count -= strlen($m[2]);
+ if ($this->interpolation($inter, false)) {
+ $content[] = $inter;
+ } else {
+ $this->count += strlen($m[2]);
+ $content[] = "@{"; // ignore it
+ }
+ } elseif ($m[2] == '\\') {
+ $content[] = $m[2];
+ if ($this->literal($delim, false)) {
+ $content[] = $delim;
+ }
+ } else {
+ $this->count -= strlen($delim);
+ break; // delim
+ }
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if ($this->literal($delim)) {
+ $out = array("string", $delim, $content);
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ protected function interpolation(&$out) {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = true;
+
+ $s = $this->seek();
+ if ($this->literal("@{") &&
+ $this->openString("}", $interp, null, array("'", '"', ";")) &&
+ $this->literal("}", false))
+ {
+ $out = array("interpolate", $interp);
+ $this->eatWhiteDefault = $oldWhite;
+ if ($this->eatWhiteDefault) $this->whitespace();
+ return true;
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+ $this->seek($s);
+ return false;
+ }
+
+ protected function unit(&$unit) {
+ // speed shortcut
+ if (isset($this->buffer[$this->count])) {
+ $char = $this->buffer[$this->count];
+ if (!ctype_digit($char) && $char != ".") return false;
+ }
+
+ if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
+ $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]);
+ return true;
+ }
+ return false;
+ }
+
+ // a # color
+ protected function color(&$out) {
+ if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
+ if (strlen($m[1]) > 7) {
+ $out = array("string", "", array($m[1]));
+ } else {
+ $out = array("raw_color", $m[1]);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ // consume an argument definition list surrounded by ()
+ // each argument is a variable name with optional value
+ // or at the end a ... or a variable named followed by ...
+ // arguments are separated by , unless a ; is in the list, then ; is the
+ // delimiter.
+ protected function argumentDef(&$args, &$isVararg) {
+ $s = $this->seek();
+ if (!$this->literal('(')) return false;
+
+ $values = array();
+ $delim = ",";
+ $method = "expressionList";
+
+ $isVararg = false;
+ while (true) {
+ if ($this->literal("...")) {
+ $isVararg = true;
+ break;
+ }
+
+ if ($this->$method($value)) {
+ if ($value[0] == "variable") {
+ $arg = array("arg", $value[1]);
+ $ss = $this->seek();
+
+ if ($this->assign() && $this->$method($rhs)) {
+ $arg[] = $rhs;
+ } else {
+ $this->seek($ss);
+ if ($this->literal("...")) {
+ $arg[0] = "rest";
+ $isVararg = true;
+ }
+ }
+
+ $values[] = $arg;
+ if ($isVararg) break;
+ continue;
+ } else {
+ $values[] = array("lit", $value);
+ }
+ }
+
+
+ if (!$this->literal($delim)) {
+ if ($delim == "," && $this->literal(";")) {
+ // found new delim, convert existing args
+ $delim = ";";
+ $method = "propertyValue";
+
+ // transform arg list
+ if (isset($values[1])) { // 2 items
+ $newList = array();
+ foreach ($values as $i => $arg) {
+ switch($arg[0]) {
+ case "arg":
+ if ($i) {
+ $this->throwError("Cannot mix ; and , as delimiter types");
+ }
+ $newList[] = $arg[2];
+ break;
+ case "lit":
+ $newList[] = $arg[1];
+ break;
+ case "rest":
+ $this->throwError("Unexpected rest before semicolon");
+ }
+ }
+
+ $newList = array("list", ", ", $newList);
+
+ switch ($values[0][0]) {
+ case "arg":
+ $newArg = array("arg", $values[0][1], $newList);
+ break;
+ case "lit":
+ $newArg = array("lit", $newList);
+ break;
+ }
+
+ } elseif ($values) { // 1 item
+ $newArg = $values[0];
+ }
+
+ if ($newArg) {
+ $values = array($newArg);
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ if (!$this->literal(')')) {
+ $this->seek($s);
+ return false;
+ }
+
+ $args = $values;
+
+ return true;
+ }
+
+ // consume a list of tags
+ // this accepts a hanging delimiter
+ protected function tags(&$tags, $simple = false, $delim = ',') {
+ $tags = array();
+ while ($this->tag($tt, $simple)) {
+ $tags[] = $tt;
+ if (!$this->literal($delim)) break;
+ }
+ if (count($tags) == 0) return false;
+
+ return true;
+ }
+
+ // list of tags of specifying mixin path
+ // optionally separated by > (lazy, accepts extra >)
+ protected function mixinTags(&$tags) {
+ $tags = array();
+ while ($this->tag($tt, true)) {
+ $tags[] = $tt;
+ $this->literal(">");
+ }
+
+ if (count($tags) == 0) return false;
+
+ return true;
+ }
+
+ // a bracketed value (contained within in a tag definition)
+ protected function tagBracket(&$parts, &$hasExpression) {
+ // speed shortcut
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") {
+ return false;
+ }
+
+ $s = $this->seek();
+
+ $hasInterpolation = false;
+
+ if ($this->literal("[", false)) {
+ $attrParts = array("[");
+ // keyword, string, operator
+ while (true) {
+ if ($this->literal("]", false)) {
+ $this->count--;
+ break; // get out early
+ }
+
+ if ($this->match('\s+', $m)) {
+ $attrParts[] = " ";
+ continue;
+ }
+ if ($this->string($str)) {
+ // escape parent selector, (yuck)
+ foreach ($str[2] as &$chunk) {
+ $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk);
+ }
+
+ $attrParts[] = $str;
+ $hasInterpolation = true;
+ continue;
+ }
+
+ if ($this->keyword($word)) {
+ $attrParts[] = $word;
+ continue;
+ }
+
+ if ($this->interpolation($inter, false)) {
+ $attrParts[] = $inter;
+ $hasInterpolation = true;
+ continue;
+ }
+
+ // operator, handles attr namespace too
+ if ($this->match('[|-~\$\*\^=]+', $m)) {
+ $attrParts[] = $m[0];
+ continue;
+ }
+
+ break;
+ }
+
+ if ($this->literal("]", false)) {
+ $attrParts[] = "]";
+ foreach ($attrParts as $part) {
+ $parts[] = $part;
+ }
+ $hasExpression = $hasExpression || $hasInterpolation;
+ return true;
+ }
+ $this->seek($s);
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ // a space separated list of selectors
+ protected function tag(&$tag, $simple = false) {
+ if ($simple)
+ $chars = '^@,:;{}\][>\(\) "\'';
+ else
+ $chars = '^@,;{}["\'';
+
+ $s = $this->seek();
+
+ $hasExpression = false;
+ $parts = array();
+ while ($this->tagBracket($parts, $hasExpression));
+
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while (true) {
+ if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
+ $parts[] = $m[1];
+ if ($simple) break;
+
+ while ($this->tagBracket($parts, $hasExpression));
+ continue;
+ }
+
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
+ if ($this->interpolation($interp)) {
+ $hasExpression = true;
+ $interp[2] = true; // don't unescape
+ $parts[] = $interp;
+ continue;
+ }
+
+ if ($this->literal("@")) {
+ $parts[] = "@";
+ continue;
+ }
+ }
+
+ if ($this->unit($unit)) { // for keyframes
+ $parts[] = $unit[1];
+ $parts[] = $unit[2];
+ continue;
+ }
+
+ break;
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+ if (!$parts) {
+ $this->seek($s);
+ return false;
+ }
+
+ if ($hasExpression) {
+ $tag = array("exp", array("string", "", $parts));
+ } else {
+ $tag = trim(implode($parts));
+ }
+
+ $this->whitespace();
+ return true;
+ }
+
+ // a css function
+ protected function func(&$func) {
+ $s = $this->seek();
+
+ if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
+ $fname = $m[1];
+
+ $sPreArgs = $this->seek();
+
+ $args = array();
+ while (true) {
+ $ss = $this->seek();
+ // this ugly nonsense is for ie filter properties
+ if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
+ $args[] = array("string", "", array($name, "=", $value));
+ } else {
+ $this->seek($ss);
+ if ($this->expressionList($value)) {
+ $args[] = $value;
+ }
+ }
+
+ if (!$this->literal(',')) break;
+ }
+ $args = array('list', ',', $args);
+
+ if ($this->literal(')')) {
+ $func = array('function', $fname, $args);
+ return true;
+ } elseif ($fname == 'url') {
+ // couldn't parse and in url? treat as string
+ $this->seek($sPreArgs);
+ if ($this->openString(")", $string) && $this->literal(")")) {
+ $func = array('function', $fname, $string);
+ return true;
+ }
+ }
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ // consume a less variable
+ protected function variable(&$name) {
+ $s = $this->seek();
+ if ($this->literal($this->lessc->vPrefix, false) &&
+ ($this->variable($sub) || $this->keyword($name)))
+ {
+ if (!empty($sub)) {
+ $name = array('variable', $sub);
+ } else {
+ $name = $this->lessc->vPrefix.$name;
+ }
+ return true;
+ }
+
+ $name = null;
+ $this->seek($s);
+ return false;
+ }
+
+ /**
+ * Consume an assignment operator
+ * Can optionally take a name that will be set to the current property name
+ */
+ protected function assign($name = null) {
+ if ($name) $this->currentProperty = $name;
+ return $this->literal(':') || $this->literal('=');
+ }
+
+ // consume a keyword
+ protected function keyword(&$word) {
+ if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
+ $word = $m[1];
+ return true;
+ }
+ return false;
+ }
+
+ // consume an end of statement delimiter
+ protected function end() {
+ if ($this->literal(';', false)) {
+ return true;
+ } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
+ // if there is end of file or a closing block next then we don't need a ;
+ return true;
+ }
+ return false;
+ }
+
+ protected function guards(&$guards) {
+ $s = $this->seek();
+
+ if (!$this->literal("when")) {
+ $this->seek($s);
+ return false;
+ }
+
+ $guards = array();
+
+ while ($this->guardGroup($g)) {
+ $guards[] = $g;
+ if (!$this->literal(",")) break;
+ }
+
+ if (count($guards) == 0) {
+ $guards = null;
+ $this->seek($s);
+ return false;
+ }
+
+ return true;
+ }
+
+ // a bunch of guards that are and'd together
+ // TODO rename to guardGroup
+ protected function guardGroup(&$guardGroup) {
+ $s = $this->seek();
+ $guardGroup = array();
+ while ($this->guard($guard)) {
+ $guardGroup[] = $guard;
+ if (!$this->literal("and")) break;
+ }
+
+ if (count($guardGroup) == 0) {
+ $guardGroup = null;
+ $this->seek($s);
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function guard(&$guard) {
+ $s = $this->seek();
+ $negate = $this->literal("not");
+
+ if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
+ $guard = $exp;
+ if ($negate) $guard = array("negate", $guard);
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ /* raw parsing functions */
+
+ protected function literal($what, $eatWhitespace = null) {
+ if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
+
+ // shortcut on single letter
+ if (!isset($what[1]) && isset($this->buffer[$this->count])) {
+ if ($this->buffer[$this->count] == $what) {
+ if (!$eatWhitespace) {
+ $this->count++;
+ return true;
+ }
+ // goes below...
+ } else {
+ return false;
+ }
+ }
+
+ if (!isset(self::$literalCache[$what])) {
+ self::$literalCache[$what] = lessc::preg_quote($what);
+ }
+
+ return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
+ }
+
+ protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
+ $s = $this->seek();
+ $items = array();
+ while ($this->$parseItem($value)) {
+ $items[] = $value;
+ if ($delim) {
+ if (!$this->literal($delim)) break;
+ }
+ }
+
+ if (count($items) == 0) {
+ $this->seek($s);
+ return false;
+ }
+
+ if ($flatten && count($items) == 1) {
+ $out = $items[0];
+ } else {
+ $out = array("list", $delim, $items);
+ }
+
+ return true;
+ }
+
+
+ // advance counter to next occurrence of $what
+ // $until - don't include $what in advance
+ // $allowNewline, if string, will be used as valid char set
+ protected function to($what, &$out, $until = false, $allowNewline = false) {
+ if (is_string($allowNewline)) {
+ $validChars = $allowNewline;
+ } else {
+ $validChars = $allowNewline ? "." : "[^\n]";
+ }
+ if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false;
+ if ($until) $this->count -= strlen($what); // give back $what
+ $out = $m[1];
+ return true;
+ }
+
+ // try to match something on head of buffer
+ protected function match($regex, &$out, $eatWhitespace = null) {
+ if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
+
+ $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais';
+ if (preg_match($r, $this->buffer, $out, null, $this->count)) {
+ $this->count += strlen($out[0]);
+ if ($eatWhitespace && $this->writeComments) $this->whitespace();
+ return true;
+ }
+ return false;
+ }
+
+ // match some whitespace
+ protected function whitespace() {
+ if ($this->writeComments) {
+ $gotWhite = false;
+ while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
+ if (isset($m[1]) && empty($this->seenComments[$this->count])) {
+ $this->append(array("comment", $m[1]));
+ $this->seenComments[$this->count] = true;
+ }
+ $this->count += strlen($m[0]);
+ $gotWhite = true;
+ }
+ return $gotWhite;
+ } else {
+ $this->match("", $m);
+ return strlen($m[0]) > 0;
+ }
+ }
+
+ // match something without consuming it
+ protected function peek($regex, &$out = null, $from=null) {
+ if (is_null($from)) $from = $this->count;
+ $r = '/'.$regex.'/Ais';
+ $result = preg_match($r, $this->buffer, $out, null, $from);
+
+ return $result;
+ }
+
+ // seek to a spot in the buffer or return where we are on no argument
+ protected function seek($where = null) {
+ if ($where === null) return $this->count;
+ else $this->count = $where;
+ return true;
+ }
+
+ /* misc functions */
+
+ public function throwError($msg = "parse error", $count = null) {
+ $count = is_null($count) ? $this->count : $count;
+
+ $line = $this->line +
+ substr_count(substr($this->buffer, 0, $count), "\n");
+
+ if (!empty($this->sourceName)) {
+ $loc = "$this->sourceName on line $line";
+ } else {
+ $loc = "line: $line";
+ }
+
+ // TODO this depends on $this->count
+ if ($this->peek("(.*?)(\n|$)", $m, $count)) {
+ throw new exception("$msg: failed at `$m[1]` $loc");
+ } else {
+ throw new exception("$msg: $loc");
+ }
+ }
+
+ protected function pushBlock($selectors=null, $type=null) {
+ $b = new stdclass;
+ $b->parent = $this->env;
+
+ $b->type = $type;
+ $b->id = self::$nextBlockId++;
+
+ $b->isVararg = false; // TODO: kill me from here
+ $b->tags = $selectors;
+
+ $b->props = array();
+ $b->children = array();
+
+ $this->env = $b;
+ return $b;
+ }
+
+ // push a block that doesn't multiply tags
+ protected function pushSpecialBlock($type) {
+ return $this->pushBlock(null, $type);
+ }
+
+ // append a property to the current block
+ protected function append($prop, $pos = null) {
+ if ($pos !== null) $prop[-1] = $pos;
+ $this->env->props[] = $prop;
+ }
+
+ // pop something off the stack
+ protected function pop() {
+ $old = $this->env;
+ $this->env = $this->env->parent;
+ return $old;
+ }
+
+ // remove comments from $text
+ // todo: make it work for all functions, not just url
+ protected function removeComments($text) {
+ $look = array(
+ 'url(', '//', '/*', '"', "'"
+ );
+
+ $out = '';
+ $min = null;
+ while (true) {
+ // find the next item
+ foreach ($look as $token) {
+ $pos = strpos($text, $token);
+ if ($pos !== false) {
+ if (!isset($min) || $pos < $min[1]) $min = array($token, $pos);
+ }
+ }
+
+ if (is_null($min)) break;
+
+ $count = $min[1];
+ $skip = 0;
+ $newlines = 0;
+ switch ($min[0]) {
+ case 'url(':
+ if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
+ $count += strlen($m[0]) - strlen($min[0]);
+ break;
+ case '"':
+ case "'":
+ if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count))
+ $count += strlen($m[0]) - 1;
+ break;
+ case '//':
+ $skip = strpos($text, "\n", $count);
+ if ($skip === false) $skip = strlen($text) - $count;
+ else $skip -= $count;
+ break;
+ case '/*':
+ if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
+ $skip = strlen($m[0]);
+ $newlines = substr_count($m[0], "\n");
+ }
+ break;
+ }
+
+ if ($skip == 0) $count += strlen($min[0]);
+
+ $out .= substr($text, 0, $count).str_repeat("\n", $newlines);
+ $text = substr($text, $count + $skip);
+
+ $min = null;
+ }
+
+ return $out.$text;
+ }
+
+}
+
+class lessc_formatter_classic {
+ public $indentChar = " ";
+
+ public $break = "\n";
+ public $open = " {";
+ public $close = "}";
+ public $selectorSeparator = ", ";
+ public $assignSeparator = ":";
+
+ public $openSingle = " { ";
+ public $closeSingle = " }";
+
+ public $disableSingle = false;
+ public $breakSelectors = false;
+
+ public $compressColors = false;
+
+ public function __construct() {
+ $this->indentLevel = 0;
+ }
+
+ public function indentStr($n = 0) {
+ return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
+ }
+
+ public function property($name, $value) {
+ return $name . $this->assignSeparator . $value . ";";
+ }
+
+ protected function isEmpty($block) {
+ if (empty($block->lines)) {
+ foreach ($block->children as $child) {
+ if (!$this->isEmpty($child)) return false;
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ public function block($block) {
+ if ($this->isEmpty($block)) return;
+
+ $inner = $pre = $this->indentStr();
+
+ $isSingle = !$this->disableSingle &&
+ is_null($block->type) && count($block->lines) == 1;
+
+ if (!empty($block->selectors)) {
+ $this->indentLevel++;
+
+ if ($this->breakSelectors) {
+ $selectorSeparator = $this->selectorSeparator . $this->break . $pre;
+ } else {
+ $selectorSeparator = $this->selectorSeparator;
+ }
+
+ echo $pre .
+ implode($selectorSeparator, $block->selectors);
+ if ($isSingle) {
+ echo $this->openSingle;
+ $inner = "";
+ } else {
+ echo $this->open . $this->break;
+ $inner = $this->indentStr();
+ }
+
+ }
+
+ if (!empty($block->lines)) {
+ $glue = $this->break.$inner;
+ echo $inner . implode($glue, $block->lines);
+ if (!$isSingle && !empty($block->children)) {
+ echo $this->break;
+ }
+ }
+
+ foreach ($block->children as $child) {
+ $this->block($child);
+ }
+
+ if (!empty($block->selectors)) {
+ if (!$isSingle && empty($block->children)) echo $this->break;
+
+ if ($isSingle) {
+ echo $this->closeSingle . $this->break;
+ } else {
+ echo $pre . $this->close . $this->break;
+ }
+
+ $this->indentLevel--;
+ }
+ }
+}
+
+class lessc_formatter_compressed extends lessc_formatter_classic {
+ public $disableSingle = true;
+ public $open = "{";
+ public $selectorSeparator = ",";
+ public $assignSeparator = ":";
+ public $break = "";
+ public $compressColors = true;
+
+ public function indentStr($n = 0) {
+ return "";
+ }
+}
+
+class lessc_formatter_lessjs extends lessc_formatter_classic {
+ public $disableSingle = true;
+ public $breakSelectors = true;
+ public $assignSeparator = ": ";
+ public $selectorSeparator = ",";
+}
+
diff --git a/plugins/jetpack/modules/custom-css/custom-css/preprocessors/scss.inc.php b/plugins/jetpack/modules/custom-css/custom-css/preprocessors/scss.inc.php
new file mode 100644
index 00000000..344b55f1
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/custom-css/preprocessors/scss.inc.php
@@ -0,0 +1,4383 @@
+<?php
+/**
+ * SCSS compiler written in PHP
+ *
+ * @copyright 2012-2013 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/gpl-license GPL-3.0
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.net/scssphp
+ */
+
+/**
+ * The scss compiler and parser.
+ *
+ * Converting SCSS to CSS is a three stage process. The incoming file is parsed
+ * by `scssc_parser` into a syntax tree, then it is compiled into another tree
+ * representing the CSS structure by `scssc`. The CSS tree is fed into a
+ * formatter, like `scssc_formatter` which then outputs CSS as a string.
+ *
+ * During the first compile, all values are *reduced*, which means that their
+ * types are brought to the lowest form before being dump as strings. This
+ * handles math equations, variable dereferences, and the like.
+ *
+ * The `parse` function of `scssc` is the entry point.
+ *
+ * In summary:
+ *
+ * The `scssc` class creates an instance of the parser, feeds it SCSS code,
+ * then transforms the resulting tree to a CSS tree. This class also holds the
+ * evaluation context, such as all available mixins and variables at any given
+ * time.
+ *
+ * The `scssc_parser` class is only concerned with parsing its input.
+ *
+ * The `scssc_formatter` takes a CSS tree, and dumps it to a formatted string,
+ * handling things like indentation.
+ */
+
+/**
+ * SCSS compiler
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class scssc {
+ static public $VERSION = "v0.0.9";
+
+ static protected $operatorNames = array(
+ '+' => "add",
+ '-' => "sub",
+ '*' => "mul",
+ '/' => "div",
+ '%' => "mod",
+
+ '==' => "eq",
+ '!=' => "neq",
+ '<' => "lt",
+ '>' => "gt",
+
+ '<=' => "lte",
+ '>=' => "gte",
+ );
+
+ static protected $namespaces = array(
+ "special" => "%",
+ "mixin" => "@",
+ "function" => "^",
+ );
+
+ static protected $unitTable = array(
+ "in" => array(
+ "in" => 1,
+ "pt" => 72,
+ "pc" => 6,
+ "cm" => 2.54,
+ "mm" => 25.4,
+ "px" => 96,
+ )
+ );
+
+ static public $true = array("keyword", "true");
+ static public $false = array("keyword", "false");
+ static public $null = array("null");
+
+ static public $defaultValue = array("keyword", "");
+ static public $selfSelector = array("self");
+
+ protected $importPaths = array("");
+ protected $importCache = array();
+
+ protected $userFunctions = array();
+
+ protected $numberPrecision = 5;
+
+ protected $formatter = "scss_formatter_nested";
+
+ public function compile($code, $name=null) {
+ $this->indentLevel = -1;
+ $this->commentsSeen = array();
+ $this->extends = array();
+ $this->extendsMap = array();
+
+ $locale = setlocale(LC_NUMERIC, 0);
+ setlocale(LC_NUMERIC, "C");
+
+ $this->parsedFiles = array();
+ $this->parser = new scss_parser($name);
+ $tree = $this->parser->parse($code);
+
+ $this->formatter = new $this->formatter();
+
+ $this->env = null;
+ $this->scope = null;
+
+ $this->compileRoot($tree);
+
+ $out = $this->formatter->format($this->scope);
+
+ setlocale(LC_NUMERIC, $locale);
+ return $out;
+ }
+
+ protected function isSelfExtend($target, $origin) {
+ foreach ($origin as $sel) {
+ if (in_array($target, $sel)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function pushExtends($target, $origin) {
+ if ($this->isSelfExtend($target, $origin)) {
+ return;
+ }
+
+ $i = count($this->extends);
+ $this->extends[] = array($target, $origin);
+
+ foreach ($target as $part) {
+ if (isset($this->extendsMap[$part])) {
+ $this->extendsMap[$part][] = $i;
+ } else {
+ $this->extendsMap[$part] = array($i);
+ }
+ }
+ }
+
+ protected function makeOutputBlock($type, $selectors = null) {
+ $out = new stdClass;
+ $out->type = $type;
+ $out->lines = array();
+ $out->children = array();
+ $out->parent = $this->scope;
+ $out->selectors = $selectors;
+ $out->depth = $this->env->depth;
+
+ return $out;
+ }
+
+ protected function matchExtendsSingle($single, &$outOrigin) {
+ $counts = array();
+ foreach ($single as $part) {
+ if (!is_string($part)) return false; // hmm
+
+ if (isset($this->extendsMap[$part])) {
+ foreach ($this->extendsMap[$part] as $idx) {
+ $counts[$idx] =
+ isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
+ }
+ }
+ }
+
+ $outOrigin = array();
+ $found = false;
+
+ foreach ($counts as $idx => $count) {
+ list($target, $origin) = $this->extends[$idx];
+
+ // check count
+ if ($count != count($target)) continue;
+
+ // check if target is subset of single
+ if (array_diff(array_intersect($single, $target), $target)) continue;
+
+ $rem = array_diff($single, $target);
+
+ foreach ($origin as $j => $new) {
+ // prevent infinite loop when target extends itself
+ foreach ($new as $new_selector) {
+ if (!array_diff($single, $new_selector)) {
+ continue 2;
+ }
+ }
+
+ $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
+ }
+
+ $outOrigin = array_merge($outOrigin, $origin);
+
+ $found = true;
+ }
+
+ return $found;
+ }
+
+ protected function combineSelectorSingle($base, $other) {
+ $tag = null;
+ $out = array();
+
+ foreach (array($base, $other) as $single) {
+ foreach ($single as $part) {
+ if (preg_match('/^[^\[.#:]/', $part)) {
+ $tag = $part;
+ } else {
+ $out[] = $part;
+ }
+ }
+ }
+
+ if ($tag) {
+ array_unshift($out, $tag);
+ }
+
+ return $out;
+ }
+
+ protected function matchExtends($selector, &$out, $from = 0, $initial=true) {
+ foreach ($selector as $i => $part) {
+ if ($i < $from) continue;
+
+ if ($this->matchExtendsSingle($part, $origin)) {
+ $before = array_slice($selector, 0, $i);
+ $after = array_slice($selector, $i + 1);
+
+ foreach ($origin as $new) {
+ $k = 0;
+
+ // remove shared parts
+ if ($initial) {
+ foreach ($before as $k => $val) {
+ if (!isset($new[$k]) || $val != $new[$k]) {
+ break;
+ }
+ }
+ }
+
+ $result = array_merge(
+ $before,
+ $k > 0 ? array_slice($new, $k) : $new,
+ $after);
+
+
+ if ($result == $selector) continue;
+ $out[] = $result;
+
+ // recursively check for more matches
+ $this->matchExtends($result, $out, $i, false);
+
+ // selector sequence merging
+ if (!empty($before) && count($new) > 1) {
+ $result2 = array_merge(
+ array_slice($new, 0, -1),
+ $k > 0 ? array_slice($before, $k) : $before,
+ array_slice($new, -1),
+ $after);
+
+ $out[] = $result2;
+ }
+ }
+ }
+ }
+ }
+
+ protected function flattenSelectors($block, $parentKey = null) {
+ if ($block->selectors) {
+ $selectors = array();
+ foreach ($block->selectors as $s) {
+ $selectors[] = $s;
+ if (!is_array($s)) continue;
+ // check extends
+ if (!empty($this->extendsMap)) {
+ $this->matchExtends($s, $selectors);
+ }
+ }
+
+ $block->selectors = array();
+ $placeholderSelector = false;
+ foreach ($selectors as $selector) {
+ if ($this->hasSelectorPlaceholder($selector)) {
+ $placeholderSelector = true;
+ continue;
+ }
+ $block->selectors[] = $this->compileSelector($selector);
+ }
+
+ if ($placeholderSelector && 0 == count($block->selectors) && null !== $parentKey) {
+ unset($block->parent->children[$parentKey]);
+ return;
+ }
+ }
+
+ foreach ($block->children as $key => $child) {
+ $this->flattenSelectors($child, $key);
+ }
+ }
+
+ protected function compileRoot($rootBlock) {
+ $this->pushEnv($rootBlock);
+ $this->scope = $this->makeOutputBlock("root");
+
+ $this->compileChildren($rootBlock->children, $this->scope);
+ $this->flattenSelectors($this->scope);
+
+ $this->popEnv();
+ }
+
+ protected function compileMedia($media) {
+ $this->pushEnv($media);
+ $parentScope = $this->mediaParent($this->scope);
+
+ $this->scope = $this->makeOutputBlock("media", array(
+ $this->compileMediaQuery($this->multiplyMedia($this->env)))
+ );
+
+ $parentScope->children[] = $this->scope;
+
+ // top level properties in a media cause it to be wrapped
+ $needsWrap = false;
+ foreach ($media->children as $child) {
+ $type = $child[0];
+ if ($type !== 'block' && $type !== 'media' && $type !== 'directive') {
+ $needsWrap = true;
+ break;
+ }
+ }
+
+ if ($needsWrap) {
+ $wrapped = (object)array(
+ "selectors" => array(),
+ "children" => $media->children
+ );
+ $media->children = array(array("block", $wrapped));
+ }
+
+ $this->compileChildren($media->children, $this->scope);
+
+ $this->scope = $this->scope->parent;
+ $this->popEnv();
+ }
+
+ protected function mediaParent($scope) {
+ while (!empty($scope->parent)) {
+ if (!empty($scope->type) && $scope->type != "media") {
+ break;
+ }
+ $scope = $scope->parent;
+ }
+
+ return $scope;
+ }
+
+ // TODO refactor compileNestedBlock and compileMedia into same thing
+ protected function compileNestedBlock($block, $selectors) {
+ $this->pushEnv($block);
+
+ $this->scope = $this->makeOutputBlock($block->type, $selectors);
+ $this->scope->parent->children[] = $this->scope;
+ $this->compileChildren($block->children, $this->scope);
+
+ $this->scope = $this->scope->parent;
+ $this->popEnv();
+ }
+
+ /**
+ * Recursively compiles a block.
+ *
+ * A block is analogous to a CSS block in most cases. A single SCSS document
+ * is encapsulated in a block when parsed, but it does not have parent tags
+ * so all of its children appear on the root level when compiled.
+ *
+ * Blocks are made up of selectors and children.
+ *
+ * The children of a block are just all the blocks that are defined within.
+ *
+ * Compiling the block involves pushing a fresh environment on the stack,
+ * and iterating through the props, compiling each one.
+ *
+ * @see scss::compileChild()
+ *
+ * @param \StdClass $block
+ */
+ protected function compileBlock($block) {
+ $env = $this->pushEnv($block);
+
+ $env->selectors =
+ array_map(array($this, "evalSelector"), $block->selectors);
+
+ $out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
+ $this->scope->children[] = $out;
+ $this->compileChildren($block->children, $out);
+
+ $this->popEnv();
+ }
+
+ // joins together .classes and #ids
+ protected function flattenSelectorSingle($single) {
+ $joined = array();
+ foreach ($single as $part) {
+ if (empty($joined) ||
+ !is_string($part) ||
+ preg_match('/[\[.:#%]/', $part))
+ {
+ $joined[] = $part;
+ continue;
+ }
+
+ if (is_array(end($joined))) {
+ $joined[] = $part;
+ } else {
+ $joined[count($joined) - 1] .= $part;
+ }
+ }
+
+ return $joined;
+ }
+
+ // replaces all the interpolates
+ protected function evalSelector($selector) {
+ return array_map(array($this, "evalSelectorPart"), $selector);
+ }
+
+ protected function evalSelectorPart($piece) {
+ foreach ($piece as &$p) {
+ if (!is_array($p)) continue;
+
+ switch ($p[0]) {
+ case "interpolate":
+ $p = $this->compileValue($p);
+ break;
+ case "string":
+ $p = $this->compileValue($p);
+ break;
+ }
+ }
+
+ return $this->flattenSelectorSingle($piece);
+ }
+
+ // compiles to string
+ // self(&) should have been replaced by now
+ protected function compileSelector($selector) {
+ if (!is_array($selector)) return $selector; // media and the like
+
+ return implode(" ", array_map(
+ array($this, "compileSelectorPart"), $selector));
+ }
+
+ protected function compileSelectorPart($piece) {
+ foreach ($piece as &$p) {
+ if (!is_array($p)) continue;
+
+ switch ($p[0]) {
+ case "self":
+ $p = "&";
+ break;
+ default:
+ $p = $this->compileValue($p);
+ break;
+ }
+ }
+
+ return implode($piece);
+ }
+
+ protected function hasSelectorPlaceholder($selector)
+ {
+ if (!is_array($selector)) return false;
+
+ foreach ($selector as $parts) {
+ foreach ($parts as $part) {
+ if ('%' == $part[0]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ protected function compileChildren($stms, $out) {
+ foreach ($stms as $stm) {
+ $ret = $this->compileChild($stm, $out);
+ if (!is_null($ret)) return $ret;
+ }
+ }
+
+ protected function compileMediaQuery($queryList) {
+ $out = "@media";
+ $first = true;
+ foreach ($queryList as $query){
+ $parts = array();
+ foreach ($query as $q) {
+ switch ($q[0]) {
+ case "mediaType":
+ $parts[] = implode(" ", array_map(array($this, "compileValue"), array_slice($q, 1)));
+ break;
+ case "mediaExp":
+ if (isset($q[2])) {
+ $parts[] = "(". $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")";
+ } else {
+ $parts[] = "(" . $this->compileValue($q[1]) . ")";
+ }
+ break;
+ }
+ }
+ if (!empty($parts)) {
+ if ($first) {
+ $first = false;
+ $out .= " ";
+ } else {
+ $out .= $this->formatter->tagSeparator;
+ }
+ $out .= implode(" and ", $parts);
+ }
+ }
+ return $out;
+ }
+
+ // returns true if the value was something that could be imported
+ protected function compileImport($rawPath, $out) {
+ if ($rawPath[0] == "string") {
+ $path = $this->compileStringContent($rawPath);
+ if ($path = $this->findImport($path)) {
+ $this->importFile($path, $out);
+ return true;
+ }
+ return false;
+ }
+ if ($rawPath[0] == "list") {
+ // handle a list of strings
+ if (count($rawPath[2]) == 0) return false;
+ foreach ($rawPath[2] as $path) {
+ if ($path[0] != "string") return false;
+ }
+
+ foreach ($rawPath[2] as $path) {
+ $this->compileImport($path, $out);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ // return a value to halt execution
+ protected function compileChild($child, $out) {
+ $this->sourcePos = isset($child[-1]) ? $child[-1] : -1;
+ $this->sourceParser = isset($child[-2]) ? $child[-2] : $this->parser;
+
+ switch ($child[0]) {
+ case "import":
+ list(,$rawPath) = $child;
+ $rawPath = $this->reduce($rawPath);
+ if (!$this->compileImport($rawPath, $out)) {
+ $out->lines[] = "@import " . $this->compileValue($rawPath) . ";";
+ }
+ break;
+ case "directive":
+ list(, $directive) = $child;
+ $s = "@" . $directive->name;
+ if (!empty($directive->value)) {
+ $s .= " " . $this->compileValue($directive->value);
+ }
+ $this->compileNestedBlock($directive, array($s));
+ break;
+ case "media":
+ $this->compileMedia($child[1]);
+ break;
+ case "block":
+ $this->compileBlock($child[1]);
+ break;
+ case "charset":
+ $out->lines[] = "@charset ".$this->compileValue($child[1]).";";
+ break;
+ case "assign":
+ list(,$name, $value) = $child;
+ if ($name[0] == "var") {
+ $isDefault = !empty($child[3]);
+
+ if ($isDefault) {
+ $existingValue = $this->get($name[1], true);
+ $shouldSet = $existingValue === true || $existingValue == self::$null;
+ }
+
+ if (!$isDefault || $shouldSet) {
+ $this->set($name[1], $this->reduce($value));
+ }
+ break;
+ }
+
+ // if the value reduces to null from something else then
+ // the property should be discarded
+ if ($value[0] != "null") {
+ $value = $this->reduce($value);
+ if ($value[0] == "null") {
+ break;
+ }
+ }
+
+ $compiledValue = $this->compileValue($value);
+ $out->lines[] = $this->formatter->property(
+ $this->compileValue($name),
+ $compiledValue);
+ break;
+ case "comment":
+ $out->lines[] = $child[1];
+ break;
+ case "mixin":
+ case "function":
+ list(,$block) = $child;
+ $this->set(self::$namespaces[$block->type] . $block->name, $block);
+ break;
+ case "extend":
+ list(, $selectors) = $child;
+ foreach ($selectors as $sel) {
+ // only use the first one
+ $sel = current($this->evalSelector($sel));
+ $this->pushExtends($sel, $out->selectors);
+ }
+ break;
+ case "if":
+ list(, $if) = $child;
+ if ($this->isTruthy($this->reduce($if->cond, true))) {
+ return $this->compileChildren($if->children, $out);
+ } else {
+ foreach ($if->cases as $case) {
+ if ($case->type == "else" ||
+ $case->type == "elseif" && $this->isTruthy($this->reduce($case->cond)))
+ {
+ return $this->compileChildren($case->children, $out);
+ }
+ }
+ }
+ break;
+ case "return":
+ return $this->reduce($child[1], true);
+ case "each":
+ list(,$each) = $child;
+ $list = $this->coerceList($this->reduce($each->list));
+ foreach ($list[2] as $item) {
+ $this->pushEnv();
+ $this->set($each->var, $item);
+ // TODO: allow return from here
+ $this->compileChildren($each->children, $out);
+ $this->popEnv();
+ }
+ break;
+ case "while":
+ list(,$while) = $child;
+ while ($this->isTruthy($this->reduce($while->cond, true))) {
+ $ret = $this->compileChildren($while->children, $out);
+ if ($ret) return $ret;
+ }
+ break;
+ case "for":
+ list(,$for) = $child;
+ $start = $this->reduce($for->start, true);
+ $start = $start[1];
+ $end = $this->reduce($for->end, true);
+ $end = $end[1];
+ $d = $start < $end ? 1 : -1;
+
+ while (true) {
+ if ((!$for->until && $start - $d == $end) ||
+ ($for->until && $start == $end))
+ {
+ break;
+ }
+
+ $this->set($for->var, array("number", $start, ""));
+ $start += $d;
+
+ $ret = $this->compileChildren($for->children, $out);
+ if ($ret) return $ret;
+ }
+
+ break;
+ case "nestedprop":
+ list(,$prop) = $child;
+ $prefixed = array();
+ $prefix = $this->compileValue($prop->prefix) . "-";
+ foreach ($prop->children as $child) {
+ if ($child[0] == "assign") {
+ array_unshift($child[1][2], $prefix);
+ }
+ if ($child[0] == "nestedprop") {
+ array_unshift($child[1]->prefix[2], $prefix);
+ }
+ $prefixed[] = $child;
+ }
+ $this->compileChildren($prefixed, $out);
+ break;
+ case "include": // including a mixin
+ list(,$name, $argValues, $content) = $child;
+ $mixin = $this->get(self::$namespaces["mixin"] . $name, false);
+ if (!$mixin) {
+ $this->throwError("Undefined mixin $name");
+ }
+
+ $callingScope = $this->env;
+
+ // push scope, apply args
+ $this->pushEnv();
+ if ($this->env->depth > 0) {
+ $this->env->depth--;
+ }
+
+ if (!is_null($content)) {
+ $content->scope = $callingScope;
+ $this->setRaw(self::$namespaces["special"] . "content", $content);
+ }
+
+ if (!is_null($mixin->args)) {
+ $this->applyArguments($mixin->args, $argValues);
+ }
+
+ foreach ($mixin->children as $child) {
+ $this->compileChild($child, $out);
+ }
+
+ $this->popEnv();
+
+ break;
+ case "mixin_content":
+ $content = $this->get(self::$namespaces["special"] . "content");
+ if (is_null($content)) {
+ $this->throwError("Expected @content inside of mixin");
+ }
+
+ $strongTypes = array('include', 'block', 'for', 'while');
+ foreach ($content->children as $child) {
+ $this->storeEnv = (in_array($child[0], $strongTypes))
+ ? null
+ : $content->scope;
+
+ $this->compileChild($child, $out);
+ }
+
+ unset($this->storeEnv);
+ break;
+ case "debug":
+ list(,$value, $pos) = $child;
+ $line = $this->parser->getLineNo($pos);
+ $value = $this->compileValue($this->reduce($value, true));
+ fwrite(STDERR, "Line $line DEBUG: $value\n");
+ break;
+ default:
+ $this->throwError("unknown child type: $child[0]");
+ }
+ }
+
+ protected function expToString($exp) {
+ list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
+ $content = array($this->reduce($left));
+ if ($whiteLeft) $content[] = " ";
+ $content[] = $op;
+ if ($whiteRight) $content[] = " ";
+ $content[] = $this->reduce($right);
+ return array("string", "", $content);
+ }
+
+ protected function isTruthy($value) {
+ return $value != self::$false && $value != self::$null;
+ }
+
+ // should $value cause its operand to eval
+ protected function shouldEval($value) {
+ switch ($value[0]) {
+ case "exp":
+ if ($value[1] == "/") {
+ return $this->shouldEval($value[2], $value[3]);
+ }
+ case "var":
+ case "fncall":
+ return true;
+ }
+ return false;
+ }
+
+ protected function reduce($value, $inExp = false) {
+ list($type) = $value;
+ switch ($type) {
+ case "exp":
+ list(, $op, $left, $right, $inParens) = $value;
+ $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
+
+ $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
+
+ $left = $this->reduce($left, true);
+ $right = $this->reduce($right, true);
+
+ // only do division in special cases
+ if ($opName == "div" && !$inParens && !$inExp) {
+ if ($left[0] != "color" && $right[0] != "color") {
+ return $this->expToString($value);
+ }
+ }
+
+ $left = $this->coerceForExpression($left);
+ $right = $this->coerceForExpression($right);
+
+ $ltype = $left[0];
+ $rtype = $right[0];
+
+ // this tries:
+ // 1. op_[op name]_[left type]_[right type]
+ // 2. op_[left type]_[right type] (passing the op as first arg
+ // 3. op_[op name]
+ $fn = "op_${opName}_${ltype}_${rtype}";
+ if (is_callable(array($this, $fn)) ||
+ (($fn = "op_${ltype}_${rtype}") &&
+ is_callable(array($this, $fn)) &&
+ $passOp = true) ||
+ (($fn = "op_${opName}") &&
+ is_callable(array($this, $fn)) &&
+ $genOp = true))
+ {
+ $unitChange = false;
+ if (!isset($genOp) &&
+ $left[0] == "number" && $right[0] == "number")
+ {
+ if ($opName == "mod" && $right[2] != "") {
+ $this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
+ }
+
+ $unitChange = true;
+ $emptyUnit = $left[2] == "" || $right[2] == "";
+ $targetUnit = "" != $left[2] ? $left[2] : $right[2];
+
+ if ($opName != "mul") {
+ $left[2] = "" != $left[2] ? $left[2] : $targetUnit;
+ $right[2] = "" != $right[2] ? $right[2] : $targetUnit;
+ }
+
+ if ($opName != "mod") {
+ $left = $this->normalizeNumber($left);
+ $right = $this->normalizeNumber($right);
+ }
+
+ if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) {
+ $targetUnit = "";
+ }
+
+ if ($opName == "mul") {
+ $left[2] = "" != $left[2] ? $left[2] : $right[2];
+ $right[2] = "" != $right[2] ? $right[2] : $left[2];
+ } elseif ($opName == "div" && $left[2] == $right[2]) {
+ $left[2] = "";
+ $right[2] = "";
+ }
+ }
+
+ $shouldEval = $inParens || $inExp;
+ if (isset($passOp)) {
+ $out = $this->$fn($op, $left, $right, $shouldEval);
+ } else {
+ $out = $this->$fn($left, $right, $shouldEval);
+ }
+
+ if (!is_null($out)) {
+ if ($unitChange && $out[0] == "number") {
+ $out = $this->coerceUnit($out, $targetUnit);
+ }
+ return $out;
+ }
+ }
+
+ return $this->expToString($value);
+ case "unary":
+ list(, $op, $exp, $inParens) = $value;
+ $inExp = $inExp || $this->shouldEval($exp);
+
+ $exp = $this->reduce($exp);
+ if ($exp[0] == "number") {
+ switch ($op) {
+ case "+":
+ return $exp;
+ case "-":
+ $exp[1] *= -1;
+ return $exp;
+ }
+ }
+
+ if ($op == "not") {
+ if ($inExp || $inParens) {
+ if ($exp == self::$false) {
+ return self::$true;
+ } else {
+ return self::$false;
+ }
+ } else {
+ $op = $op . " ";
+ }
+ }
+
+ return array("string", "", array($op, $exp));
+ case "var":
+ list(, $name) = $value;
+ return $this->reduce($this->get($name));
+ case "list":
+ foreach ($value[2] as &$item) {
+ $item = $this->reduce($item);
+ }
+ return $value;
+ case "string":
+ foreach ($value[2] as &$item) {
+ if (is_array($item)) {
+ $item = $this->reduce($item);
+ }
+ }
+ return $value;
+ case "interpolate":
+ $value[1] = $this->reduce($value[1]);
+ return $value;
+ case "fncall":
+ list(,$name, $argValues) = $value;
+
+ // user defined function?
+ $func = $this->get(self::$namespaces["function"] . $name, false);
+ if ($func) {
+ $this->pushEnv();
+
+ // set the args
+ if (isset($func->args)) {
+ $this->applyArguments($func->args, $argValues);
+ }
+
+ // throw away lines and children
+ $tmp = (object)array(
+ "lines" => array(),
+ "children" => array()
+ );
+ $ret = $this->compileChildren($func->children, $tmp);
+ $this->popEnv();
+
+ return is_null($ret) ? self::$defaultValue : $ret;
+ }
+
+ // built in function
+ if ($this->callBuiltin($name, $argValues, $returnValue)) {
+ return $returnValue;
+ }
+
+ // need to flatten the arguments into a list
+ $listArgs = array();
+ foreach ((array)$argValues as $arg) {
+ if (empty($arg[0])) {
+ $listArgs[] = $this->reduce($arg[1]);
+ }
+ }
+ return array("function", $name, array("list", ",", $listArgs));
+ default:
+ return $value;
+ }
+ }
+
+ public function normalizeValue($value) {
+ $value = $this->coerceForExpression($this->reduce($value));
+ list($type) = $value;
+
+ switch ($type) {
+ case "list":
+ $value = $this->extractInterpolation($value);
+ if ($value[0] != "list") {
+ return array("keyword", $this->compileValue($value));
+ }
+ foreach ($value[2] as $key => $item) {
+ $value[2][$key] = $this->normalizeValue($item);
+ }
+ return $value;
+ case "number":
+ return $this->normalizeNumber($value);
+ default:
+ return $value;
+ }
+ }
+
+ // just does physical lengths for now
+ protected function normalizeNumber($number) {
+ list(, $value, $unit) = $number;
+ if (isset(self::$unitTable["in"][$unit])) {
+ $conv = self::$unitTable["in"][$unit];
+ return array("number", $value / $conv, "in");
+ }
+ return $number;
+ }
+
+ // $number should be normalized
+ protected function coerceUnit($number, $unit) {
+ list(, $value, $baseUnit) = $number;
+ if (isset(self::$unitTable[$baseUnit][$unit])) {
+ $value = $value * self::$unitTable[$baseUnit][$unit];
+ }
+
+ return array("number", $value, $unit);
+ }
+
+ protected function op_add_number_number($left, $right) {
+ return array("number", $left[1] + $right[1], $left[2]);
+ }
+
+ protected function op_mul_number_number($left, $right) {
+ return array("number", $left[1] * $right[1], $left[2]);
+ }
+
+ protected function op_sub_number_number($left, $right) {
+ return array("number", $left[1] - $right[1], $left[2]);
+ }
+
+ protected function op_div_number_number($left, $right) {
+ return array("number", $left[1] / $right[1], $left[2]);
+ }
+
+ protected function op_mod_number_number($left, $right) {
+ return array("number", $left[1] % $right[1], $left[2]);
+ }
+
+ // adding strings
+ protected function op_add($left, $right) {
+ if ($strLeft = $this->coerceString($left)) {
+ if ($right[0] == "string") {
+ $right[1] = "";
+ }
+ $strLeft[2][] = $right;
+ return $strLeft;
+ }
+
+ if ($strRight = $this->coerceString($right)) {
+ if ($left[0] == "string") {
+ $left[1] = "";
+ }
+ array_unshift($strRight[2], $left);
+ return $strRight;
+ }
+ }
+
+ protected function op_and($left, $right, $shouldEval) {
+ if (!$shouldEval) return;
+ if ($left != self::$false) return $right;
+ return $left;
+ }
+
+ protected function op_or($left, $right, $shouldEval) {
+ if (!$shouldEval) return;
+ if ($left != self::$false) return $left;
+ return $right;
+ }
+
+ protected function op_color_color($op, $left, $right) {
+ $out = array('color');
+ foreach (range(1, 3) as $i) {
+ $lval = isset($left[$i]) ? $left[$i] : 0;
+ $rval = isset($right[$i]) ? $right[$i] : 0;
+ switch ($op) {
+ case '+':
+ $out[] = $lval + $rval;
+ break;
+ case '-':
+ $out[] = $lval - $rval;
+ break;
+ case '*':
+ $out[] = $lval * $rval;
+ break;
+ case '%':
+ $out[] = $lval % $rval;
+ break;
+ case '/':
+ if ($rval == 0) {
+ $this->throwError("color: Can't divide by zero");
+ }
+ $out[] = $lval / $rval;
+ break;
+ case "==":
+ return $this->op_eq($left, $right);
+ case "!=":
+ return $this->op_neq($left, $right);
+ default:
+ $this->throwError("color: unknown op $op");
+ }
+ }
+
+ if (isset($left[4])) $out[4] = $left[4];
+ elseif (isset($right[4])) $out[4] = $right[4];
+
+ return $this->fixColor($out);
+ }
+
+ protected function op_color_number($op, $left, $right) {
+ $value = $right[1];
+ return $this->op_color_color($op, $left,
+ array("color", $value, $value, $value));
+ }
+
+ protected function op_number_color($op, $left, $right) {
+ $value = $left[1];
+ return $this->op_color_color($op,
+ array("color", $value, $value, $value), $right);
+ }
+
+ protected function op_eq($left, $right) {
+ if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
+ $lStr[1] = "";
+ $rStr[1] = "";
+ return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr));
+ }
+
+ return $this->toBool($left == $right);
+ }
+
+ protected function op_neq($left, $right) {
+ return $this->toBool($left != $right);
+ }
+
+ protected function op_gte_number_number($left, $right) {
+ return $this->toBool($left[1] >= $right[1]);
+ }
+
+ protected function op_gt_number_number($left, $right) {
+ return $this->toBool($left[1] > $right[1]);
+ }
+
+ protected function op_lte_number_number($left, $right) {
+ return $this->toBool($left[1] <= $right[1]);
+ }
+
+ protected function op_lt_number_number($left, $right) {
+ return $this->toBool($left[1] < $right[1]);
+ }
+
+ public function toBool($thing) {
+ return $thing ? self::$true : self::$false;
+ }
+
+ /**
+ * Compiles a primitive value into a CSS property value.
+ *
+ * Values in scssphp are typed by being wrapped in arrays, their format is
+ * typically:
+ *
+ * array(type, contents [, additional_contents]*)
+ *
+ * The input is expected to be reduced. This function will not work on
+ * things like expressions and variables.
+ *
+ * @param array $value
+ */
+ protected function compileValue($value) {
+ $value = $this->reduce($value);
+
+ list($type) = $value;
+ switch ($type) {
+ case "keyword":
+ return $value[1];
+ case "color":
+ // [1] - red component (either number for a %)
+ // [2] - green component
+ // [3] - blue component
+ // [4] - optional alpha component
+ list(, $r, $g, $b) = $value;
+
+ $r = round($r);
+ $g = round($g);
+ $b = round($b);
+
+ if (count($value) == 5 && $value[4] != 1) { // rgba
+ return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')';
+ }
+
+ $h = sprintf("#%02x%02x%02x", $r, $g, $b);
+
+ // Converting hex color to short notation (e.g. #003399 to #039)
+ if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
+ $h = '#' . $h[1] . $h[3] . $h[5];
+ }
+
+ return $h;
+ case "number":
+ return round($value[1], $this->numberPrecision) . $value[2];
+ case "string":
+ return $value[1] . $this->compileStringContent($value) . $value[1];
+ case "function":
+ $args = !empty($value[2]) ? $this->compileValue($value[2]) : "";
+ return "$value[1]($args)";
+ case "list":
+ $value = $this->extractInterpolation($value);
+ if ($value[0] != "list") return $this->compileValue($value);
+
+ list(, $delim, $items) = $value;
+
+ $filtered = array();
+ foreach ($items as $item) {
+ if ($item[0] == "null") continue;
+ $filtered[] = $this->compileValue($item);
+ }
+
+ return implode("$delim ", $filtered);
+ case "interpolated": # node created by extractInterpolation
+ list(, $interpolate, $left, $right) = $value;
+ list(,, $whiteLeft, $whiteRight) = $interpolate;
+
+ $left = count($left[2]) > 0 ?
+ $this->compileValue($left).$whiteLeft : "";
+
+ $right = count($right[2]) > 0 ?
+ $whiteRight.$this->compileValue($right) : "";
+
+ return $left.$this->compileValue($interpolate).$right;
+
+ case "interpolate": # raw parse node
+ list(, $exp) = $value;
+
+ // strip quotes if it's a string
+ $reduced = $this->reduce($exp);
+ switch ($reduced[0]) {
+ case "string":
+ $reduced = array("keyword",
+ $this->compileStringContent($reduced));
+ break;
+ case "null":
+ $reduced = array("keyword", "");
+ }
+
+ return $this->compileValue($reduced);
+ case "null":
+ return "null";
+ default:
+ $this->throwError("unknown value type: $type");
+ }
+ }
+
+ protected function compileStringContent($string) {
+ $parts = array();
+ foreach ($string[2] as $part) {
+ if (is_array($part)) {
+ $parts[] = $this->compileValue($part);
+ } else {
+ $parts[] = $part;
+ }
+ }
+
+ return implode($parts);
+ }
+
+ // doesn't need to be recursive, compileValue will handle that
+ protected function extractInterpolation($list) {
+ $items = $list[2];
+ foreach ($items as $i => $item) {
+ if ($item[0] == "interpolate") {
+ $before = array("list", $list[1], array_slice($items, 0, $i));
+ $after = array("list", $list[1], array_slice($items, $i + 1));
+ return array("interpolated", $item, $before, $after);
+ }
+ }
+ return $list;
+ }
+
+ // find the final set of selectors
+ protected function multiplySelectors($env) {
+ $envs = array();
+ while (null !== $env) {
+ if (!empty($env->selectors)) {
+ $envs[] = $env;
+ }
+ $env = $env->parent;
+ };
+
+ $selectors = array();
+ $parentSelectors = array(array());
+ while ($env = array_pop($envs)) {
+ $selectors = array();
+ foreach ($env->selectors as $selector) {
+ foreach ($parentSelectors as $parent) {
+ $selectors[] = $this->joinSelectors($parent, $selector);
+ }
+ }
+ $parentSelectors = $selectors;
+ }
+
+ return $selectors;
+ }
+
+ // looks for & to replace, or append parent before child
+ protected function joinSelectors($parent, $child) {
+ $setSelf = false;
+ $out = array();
+ foreach ($child as $part) {
+ $newPart = array();
+ foreach ($part as $p) {
+ if ($p == self::$selfSelector) {
+ $setSelf = true;
+ foreach ($parent as $i => $parentPart) {
+ if ($i > 0) {
+ $out[] = $newPart;
+ $newPart = array();
+ }
+
+ foreach ($parentPart as $pp) {
+ $newPart[] = $pp;
+ }
+ }
+ } else {
+ $newPart[] = $p;
+ }
+ }
+
+ $out[] = $newPart;
+ }
+
+ return $setSelf ? $out : array_merge($parent, $child);
+ }
+
+ protected function multiplyMedia($env, $childQueries = null) {
+ if (is_null($env) ||
+ !empty($env->block->type) && $env->block->type != "media")
+ {
+ return $childQueries;
+ }
+
+ // plain old block, skip
+ if (empty($env->block->type)) {
+ return $this->multiplyMedia($env->parent, $childQueries);
+ }
+
+ $parentQueries = $env->block->queryList;
+ if ($childQueries == null) {
+ $childQueries = $parentQueries;
+ } else {
+ $originalQueries = $childQueries;
+ $childQueries = array();
+
+ foreach ($parentQueries as $parentQuery){
+ foreach ($originalQueries as $childQuery) {
+ $childQueries []= array_merge($parentQuery, $childQuery);
+ }
+ }
+ }
+
+ return $this->multiplyMedia($env->parent, $childQueries);
+ }
+
+ // convert something to list
+ protected function coerceList($item, $delim = ",") {
+ if (!is_null($item) && $item[0] == "list") {
+ return $item;
+ }
+
+ return array("list", $delim, is_null($item) ? array(): array($item));
+ }
+
+ protected function applyArguments($argDef, $argValues) {
+ $hasVariable = false;
+ $args = array();
+ foreach ($argDef as $i => $arg) {
+ list($name, $default, $isVariable) = $argDef[$i];
+ $args[$name] = array($i, $name, $default, $isVariable);
+ $hasVariable |= $isVariable;
+ }
+
+ $keywordArgs = array();
+ $deferredKeywordArgs = array();
+ $remaining = array();
+ // assign the keyword args
+ foreach ((array) $argValues as $arg) {
+ if (!empty($arg[0])) {
+ if (!isset($args[$arg[0][1]])) {
+ if ($hasVariable) {
+ $deferredKeywordArgs[$arg[0][1]] = $arg[1];
+ } else {
+ $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
+ }
+ } elseif ($args[$arg[0][1]][0] < count($remaining)) {
+ $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
+ } else {
+ $keywordArgs[$arg[0][1]] = $arg[1];
+ }
+ } elseif (count($keywordArgs)) {
+ $this->throwError('Positional arguments must come before keyword arguments.');
+ } elseif ($arg[2] == true) {
+ $val = $this->reduce($arg[1], true);
+ if ($val[0] == "list") {
+ foreach ($val[2] as $name => $item) {
+ if (!is_numeric($name)) {
+ $keywordArgs[$name] = $item;
+ } else {
+ $remaining[] = $item;
+ }
+ }
+ } else {
+ $remaining[] = $val;
+ }
+ } else {
+ $remaining[] = $arg[1];
+ }
+ }
+
+ foreach ($args as $arg) {
+ list($i, $name, $default, $isVariable) = $arg;
+ if ($isVariable) {
+ $val = array("list", ",", array());
+ for ($count = count($remaining); $i < $count; $i++) {
+ $val[2][] = $remaining[$i];
+ }
+ foreach ($deferredKeywordArgs as $itemName => $item) {
+ $val[2][$itemName] = $item;
+ }
+ } elseif (isset($remaining[$i])) {
+ $val = $remaining[$i];
+ } elseif (isset($keywordArgs[$name])) {
+ $val = $keywordArgs[$name];
+ } elseif (!empty($default)) {
+ $val = $default;
+ } else {
+ $this->throwError("Missing argument $name");
+ }
+
+ $this->set($name, $this->reduce($val, true), true);
+ }
+ }
+
+ protected function pushEnv($block=null) {
+ $env = new stdClass;
+ $env->parent = $this->env;
+ $env->store = array();
+ $env->block = $block;
+ $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
+
+ $this->env = $env;
+ return $env;
+ }
+
+ protected function normalizeName($name) {
+ return str_replace("-", "_", $name);
+ }
+
+ protected function getStoreEnv() {
+ return isset($this->storeEnv) ? $this->storeEnv : $this->env;
+ }
+
+ protected function set($name, $value, $shadow=false) {
+ $name = $this->normalizeName($name);
+
+ if ($shadow) {
+ $this->setRaw($name, $value);
+ } else {
+ $this->setExisting($name, $value);
+ }
+ }
+
+ protected function setExisting($name, $value, $env = null) {
+ if (is_null($env)) $env = $this->getStoreEnv();
+
+ if (isset($env->store[$name]) || is_null($env->parent)) {
+ $env->store[$name] = $value;
+ } else {
+ $this->setExisting($name, $value, $env->parent);
+ }
+ }
+
+ protected function setRaw($name, $value) {
+ $env = $this->getStoreEnv();
+ $env->store[$name] = $value;
+ }
+
+ public function get($name, $defaultValue = null, $env = null) {
+ $name = $this->normalizeName($name);
+
+ if (is_null($env)) $env = $this->getStoreEnv();
+ if (is_null($defaultValue)) $defaultValue = self::$defaultValue;
+
+ if (isset($env->store[$name])) {
+ return $env->store[$name];
+ } elseif (isset($env->parent)) {
+ return $this->get($name, $defaultValue, $env->parent);
+ }
+
+ return $defaultValue; // found nothing
+ }
+
+ protected function popEnv() {
+ $env = $this->env;
+ $this->env = $this->env->parent;
+ return $env;
+ }
+
+ public function getParsedFiles() {
+ return $this->parsedFiles;
+ }
+
+ public function addImportPath($path) {
+ $this->importPaths[] = $path;
+ }
+
+ public function setImportPaths($path) {
+ $this->importPaths = (array)$path;
+ }
+
+ public function setNumberPrecision($numberPrecision) {
+ $this->numberPrecision = $numberPrecision;
+ }
+
+ public function setFormatter($formatterName) {
+ $this->formatter = $formatterName;
+ }
+
+ public function registerFunction($name, $func) {
+ $this->userFunctions[$this->normalizeName($name)] = $func;
+ }
+
+ public function unregisterFunction($name) {
+ unset($this->userFunctions[$this->normalizeName($name)]);
+ }
+
+ protected function importFile($path, $out) {
+ // see if tree is cached
+ $realPath = realpath($path);
+ if (isset($this->importCache[$realPath])) {
+ $tree = $this->importCache[$realPath];
+ } else {
+ $code = file_get_contents($path);
+ $parser = new scss_parser($path, false);
+ $tree = $parser->parse($code);
+ $this->parsedFiles[] = $path;
+
+ $this->importCache[$realPath] = $tree;
+ }
+
+ $pi = pathinfo($path);
+ array_unshift($this->importPaths, $pi['dirname']);
+ $this->compileChildren($tree->children, $out);
+ array_shift($this->importPaths);
+ }
+
+ // results the file path for an import url if it exists
+ public function findImport($url) {
+ $urls = array();
+
+ // for "normal" scss imports (ignore vanilla css and external requests)
+ if (!preg_match('/\.css|^http:\/\/$/', $url)) {
+ // try both normal and the _partial filename
+ $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
+ }
+
+ foreach ($this->importPaths as $dir) {
+ if (is_string($dir)) {
+ // check urls for normal import paths
+ foreach ($urls as $full) {
+ $full = $dir .
+ (!empty($dir) && substr($dir, -1) != '/' ? '/' : '') .
+ $full;
+
+ if ($this->fileExists($file = $full.'.scss') ||
+ $this->fileExists($file = $full))
+ {
+ return $file;
+ }
+ }
+ } else {
+ // check custom callback for import path
+ $file = call_user_func($dir,$url,$this);
+ if ($file !== null) {
+ return $file;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ protected function fileExists($name) {
+ return is_file($name);
+ }
+
+ protected function callBuiltin($name, $args, &$returnValue) {
+ // try a lib function
+ $name = $this->normalizeName($name);
+ $libName = "lib_".$name;
+ $f = array($this, $libName);
+ $prototype = isset(self::$$libName) ? self::$$libName : null;
+
+ if (is_callable($f)) {
+ $sorted = $this->sortArgs($prototype, $args);
+ foreach ($sorted as &$val) {
+ $val = $this->reduce($val, true);
+ }
+ $returnValue = call_user_func($f, $sorted, $this);
+ } elseif (isset($this->userFunctions[$name])) {
+ // see if we can find a user function
+ $fn = $this->userFunctions[$name];
+
+ foreach ($args as &$val) {
+ $val = $this->reduce($val[1], true);
+ }
+
+ $returnValue = call_user_func($fn, $args, $this);
+ }
+
+ if (isset($returnValue)) {
+ // coerce a php value into a scss one
+ if (is_numeric($returnValue)) {
+ $returnValue = array('number', $returnValue, "");
+ } elseif (is_bool($returnValue)) {
+ $returnValue = $returnValue ? self::$true : self::$false;
+ } elseif (!is_array($returnValue)) {
+ $returnValue = array('keyword', $returnValue);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ // sorts any keyword arguments
+ // TODO: merge with apply arguments
+ protected function sortArgs($prototype, $args) {
+ $keyArgs = array();
+ $posArgs = array();
+
+ foreach ($args as $arg) {
+ list($key, $value) = $arg;
+ $key = $key[1];
+ if (empty($key)) {
+ $posArgs[] = $value;
+ } else {
+ $keyArgs[$key] = $value;
+ }
+ }
+
+ if (is_null($prototype)) return $posArgs;
+
+ $finalArgs = array();
+ foreach ($prototype as $i => $names) {
+ if (isset($posArgs[$i])) {
+ $finalArgs[] = $posArgs[$i];
+ continue;
+ }
+
+ $set = false;
+ foreach ((array)$names as $name) {
+ if (isset($keyArgs[$name])) {
+ $finalArgs[] = $keyArgs[$name];
+ $set = true;
+ break;
+ }
+ }
+
+ if (!$set) {
+ $finalArgs[] = null;
+ }
+ }
+
+ return $finalArgs;
+ }
+
+ protected function coerceForExpression($value) {
+ if ($color = $this->coerceColor($value)) {
+ return $color;
+ }
+
+ return $value;
+ }
+
+ protected function coerceColor($value) {
+ switch ($value[0]) {
+ case "color": return $value;
+ case "keyword":
+ $name = $value[1];
+ if (isset(self::$cssColors[$name])) {
+ @list($r, $g, $b, $a) = explode(',', self::$cssColors[$name]);
+ return isset($a)
+ ? array('color', (int) $r, (int) $g, (int) $b, (int) $a)
+ : array('color', (int) $r, (int) $g, (int) $b);
+ }
+ return null;
+ }
+
+ return null;
+ }
+
+ protected function coerceString($value) {
+ switch ($value[0]) {
+ case "string":
+ return $value;
+ case "keyword":
+ return array("string", "", array($value[1]));
+ }
+ return null;
+ }
+
+ public function assertList($value) {
+ if ($value[0] != "list")
+ $this->throwError("expecting list");
+ return $value;
+ }
+
+ public function assertColor($value) {
+ if ($color = $this->coerceColor($value)) return $color;
+ $this->throwError("expecting color");
+ }
+
+ public function assertNumber($value) {
+ if ($value[0] != "number")
+ $this->throwError("expecting number");
+ return $value[1];
+ }
+
+ protected function coercePercent($value) {
+ if ($value[0] == "number") {
+ if ($value[2] == "%") {
+ return $value[1] / 100;
+ }
+ return $value[1];
+ }
+ return 0;
+ }
+
+ // make sure a color's components don't go out of bounds
+ protected function fixColor($c) {
+ foreach (range(1, 3) as $i) {
+ if ($c[$i] < 0) $c[$i] = 0;
+ if ($c[$i] > 255) $c[$i] = 255;
+ }
+
+ return $c;
+ }
+
+ public function toHSL($red, $green, $blue) {
+ $r = $red / 255;
+ $g = $green / 255;
+ $b = $blue / 255;
+
+ $min = min($r, $g, $b);
+ $max = max($r, $g, $b);
+ $d = $max - $min;
+ $l = ($min + $max) / 2;
+
+ if ($min == $max) {
+ $s = $h = 0;
+ } else {
+ if ($l < 0.5)
+ $s = $d / (2 * $l);
+ else
+ $s = $d / (2 - 2 * $l);
+
+ if ($r == $max)
+ $h = 60 * ($g - $b) / $d;
+ elseif ($g == $max)
+ $h = 60 * ($b - $r) / $d + 120;
+ elseif ($b == $max)
+ $h = 60 * ($r - $g) / $d + 240;
+ }
+
+ return array('hsl', fmod($h, 360), $s * 100, $l * 100);
+ }
+
+ public function hueToRGB($m1, $m2, $h) {
+ if ($h < 0)
+ $h += 1;
+ elseif ($h > 1)
+ $h -= 1;
+
+ if ($h * 6 < 1)
+ return $m1 + ($m2 - $m1) * $h * 6;
+
+ if ($h * 2 < 1)
+ return $m2;
+
+ if ($h * 3 < 2)
+ return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
+
+ return $m1;
+ }
+
+ // H from 0 to 360, S and L from 0 to 100
+ public function toRGB($hue, $saturation, $lightness) {
+ if ($hue < 0) {
+ $hue += 360;
+ }
+
+ $h = $hue / 360;
+ $s = min(100, max(0, $saturation)) / 100;
+ $l = min(100, max(0, $lightness)) / 100;
+
+ $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
+ $m1 = $l * 2 - $m2;
+
+ $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
+ $g = $this->hueToRGB($m1, $m2, $h) * 255;
+ $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
+
+ $out = array('color', $r, $g, $b);
+ return $out;
+ }
+
+ // Built in functions
+
+ protected static $lib_if = array("condition", "if-true", "if-false");
+ protected function lib_if($args) {
+ list($cond,$t, $f) = $args;
+ if ($cond == self::$false) return $f;
+ return $t;
+ }
+
+ protected static $lib_index = array("list", "value");
+ protected function lib_index($args) {
+ list($list, $value) = $args;
+ $list = $this->assertList($list);
+
+ $values = array();
+ foreach ($list[2] as $item) {
+ $values[] = $this->normalizeValue($item);
+ }
+ $key = array_search($this->normalizeValue($value), $values);
+
+ return false === $key ? false : $key + 1;
+ }
+
+ protected static $lib_rgb = array("red", "green", "blue");
+ protected function lib_rgb($args) {
+ list($r,$g,$b) = $args;
+ return array("color", $r[1], $g[1], $b[1]);
+ }
+
+ protected static $lib_rgba = array(
+ array("red", "color"),
+ "green", "blue", "alpha");
+ protected function lib_rgba($args) {
+ if ($color = $this->coerceColor($args[0])) {
+ $num = is_null($args[1]) ? $args[3] : $args[1];
+ $alpha = $this->assertNumber($num);
+ $color[4] = $alpha;
+ return $color;
+ }
+
+ list($r,$g,$b, $a) = $args;
+ return array("color", $r[1], $g[1], $b[1], $a[1]);
+ }
+
+ // helper function for adjust_color, change_color, and scale_color
+ protected function alter_color($args, $fn) {
+ $color = $this->assertColor($args[0]);
+
+ foreach (array(1,2,3,7) as $i) {
+ if (!is_null($args[$i])) {
+ $val = $this->assertNumber($args[$i]);
+ $ii = $i == 7 ? 4 : $i; // alpha
+ $color[$ii] =
+ $this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
+ }
+ }
+
+ if (!is_null($args[4]) || !is_null($args[5]) || !is_null($args[6])) {
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+ foreach (array(4,5,6) as $i) {
+ if (!is_null($args[$i])) {
+ $val = $this->assertNumber($args[$i]);
+ $hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i);
+ }
+ }
+
+ $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
+ if (isset($color[4])) $rgb[4] = $color[4];
+ $color = $rgb;
+ }
+
+ return $color;
+ }
+
+ protected static $lib_adjust_color = array(
+ "color", "red", "green", "blue",
+ "hue", "saturation", "lightness", "alpha"
+ );
+ protected function adjust_color_helper($base, $alter, $i) {
+ return $base += $alter;
+ }
+ protected function lib_adjust_color($args) {
+ return $this->alter_color($args, "adjust_color_helper");
+ }
+
+ protected static $lib_change_color = array(
+ "color", "red", "green", "blue",
+ "hue", "saturation", "lightness", "alpha"
+ );
+ protected function change_color_helper($base, $alter, $i) {
+ return $alter;
+ }
+ protected function lib_change_color($args) {
+ return $this->alter_color($args, "change_color_helper");
+ }
+
+ protected static $lib_scale_color = array(
+ "color", "red", "green", "blue",
+ "hue", "saturation", "lightness", "alpha"
+ );
+ protected function scale_color_helper($base, $scale, $i) {
+ // 1,2,3 - rgb
+ // 4, 5, 6 - hsl
+ // 7 - a
+ switch ($i) {
+ case 1:
+ case 2:
+ case 3:
+ $max = 255; break;
+ case 4:
+ $max = 360; break;
+ case 7:
+ $max = 1; break;
+ default:
+ $max = 100;
+ }
+
+ $scale = $scale / 100;
+ if ($scale < 0) {
+ return $base * $scale + $base;
+ } else {
+ return ($max - $base) * $scale + $base;
+ }
+ }
+ protected function lib_scale_color($args) {
+ return $this->alter_color($args, "scale_color_helper");
+ }
+
+ protected static $lib_ie_hex_str = array("color");
+ protected function lib_ie_hex_str($args) {
+ $color = $this->coerceColor($args[0]);
+ $color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
+
+ return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
+ }
+
+ protected static $lib_red = array("color");
+ protected function lib_red($args) {
+ $color = $this->coerceColor($args[0]);
+ return $color[1];
+ }
+
+ protected static $lib_green = array("color");
+ protected function lib_green($args) {
+ $color = $this->coerceColor($args[0]);
+ return $color[2];
+ }
+
+ protected static $lib_blue = array("color");
+ protected function lib_blue($args) {
+ $color = $this->coerceColor($args[0]);
+ return $color[3];
+ }
+
+ protected static $lib_alpha = array("color");
+ protected function lib_alpha($args) {
+ if ($color = $this->coerceColor($args[0])) {
+ return isset($color[4]) ? $color[4] : 1;
+ }
+
+ // this might be the IE function, so return value unchanged
+ return null;
+ }
+
+ protected static $lib_opacity = array("color");
+ protected function lib_opacity($args) {
+ $value = $args[0];
+ if ($value[0] === 'number') return null;
+ return $this->lib_alpha($args);
+ }
+
+ // mix two colors
+ protected static $lib_mix = array("color-1", "color-2", "weight");
+ protected function lib_mix($args) {
+ list($first, $second, $weight) = $args;
+ $first = $this->assertColor($first);
+ $second = $this->assertColor($second);
+
+ if (is_null($weight)) {
+ $weight = 0.5;
+ } else {
+ $weight = $this->coercePercent($weight);
+ }
+
+ $firstAlpha = isset($first[4]) ? $first[4] : 1;
+ $secondAlpha = isset($second[4]) ? $second[4] : 1;
+
+ $w = $weight * 2 - 1;
+ $a = $firstAlpha - $secondAlpha;
+
+ $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
+ $w2 = 1.0 - $w1;
+
+ $new = array('color',
+ $w1 * $first[1] + $w2 * $second[1],
+ $w1 * $first[2] + $w2 * $second[2],
+ $w1 * $first[3] + $w2 * $second[3],
+ );
+
+ if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
+ $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
+ }
+
+ return $this->fixColor($new);
+ }
+
+ protected static $lib_hsl = array("hue", "saturation", "lightness");
+ protected function lib_hsl($args) {
+ list($h, $s, $l) = $args;
+ return $this->toRGB($h[1], $s[1], $l[1]);
+ }
+
+ protected static $lib_hsla = array("hue", "saturation",
+ "lightness", "alpha");
+ protected function lib_hsla($args) {
+ list($h, $s, $l, $a) = $args;
+ $color = $this->toRGB($h[1], $s[1], $l[1]);
+ $color[4] = $a[1];
+ return $color;
+ }
+
+ protected static $lib_hue = array("color");
+ protected function lib_hue($args) {
+ $color = $this->assertColor($args[0]);
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+ return array("number", $hsl[1], "deg");
+ }
+
+ protected static $lib_saturation = array("color");
+ protected function lib_saturation($args) {
+ $color = $this->assertColor($args[0]);
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+ return array("number", $hsl[2], "%");
+ }
+
+ protected static $lib_lightness = array("color");
+ protected function lib_lightness($args) {
+ $color = $this->assertColor($args[0]);
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+ return array("number", $hsl[3], "%");
+ }
+
+ protected function adjustHsl($color, $idx, $amount) {
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+ $hsl[$idx] += $amount;
+ $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
+ if (isset($color[4])) $out[4] = $color[4];
+ return $out;
+ }
+
+ protected static $lib_adjust_hue = array("color", "degrees");
+ protected function lib_adjust_hue($args) {
+ $color = $this->assertColor($args[0]);
+ $degrees = $this->assertNumber($args[1]);
+ return $this->adjustHsl($color, 1, $degrees);
+ }
+
+ protected static $lib_lighten = array("color", "amount");
+ protected function lib_lighten($args) {
+ $color = $this->assertColor($args[0]);
+ $amount = 100*$this->coercePercent($args[1]);
+ return $this->adjustHsl($color, 3, $amount);
+ }
+
+ protected static $lib_darken = array("color", "amount");
+ protected function lib_darken($args) {
+ $color = $this->assertColor($args[0]);
+ $amount = 100*$this->coercePercent($args[1]);
+ return $this->adjustHsl($color, 3, -$amount);
+ }
+
+ protected static $lib_saturate = array("color", "amount");
+ protected function lib_saturate($args) {
+ $value = $args[0];
+ if ($value[0] === 'number') return null;
+ $color = $this->assertColor($value);
+ $amount = 100*$this->coercePercent($args[1]);
+ return $this->adjustHsl($color, 2, $amount);
+ }
+
+ protected static $lib_desaturate = array("color", "amount");
+ protected function lib_desaturate($args) {
+ $color = $this->assertColor($args[0]);
+ $amount = 100*$this->coercePercent($args[1]);
+ return $this->adjustHsl($color, 2, -$amount);
+ }
+
+ protected static $lib_grayscale = array("color");
+ protected function lib_grayscale($args) {
+ $value = $args[0];
+ if ($value[0] === 'number') return null;
+ return $this->adjustHsl($this->assertColor($value), 2, -100);
+ }
+
+ protected static $lib_complement = array("color");
+ protected function lib_complement($args) {
+ return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
+ }
+
+ protected static $lib_invert = array("color");
+ protected function lib_invert($args) {
+ $value = $args[0];
+ if ($value[0] === 'number') return null;
+ $color = $this->assertColor($value);
+ $color[1] = 255 - $color[1];
+ $color[2] = 255 - $color[2];
+ $color[3] = 255 - $color[3];
+ return $color;
+ }
+
+ // increases opacity by amount
+ protected static $lib_opacify = array("color", "amount");
+ protected function lib_opacify($args) {
+ $color = $this->assertColor($args[0]);
+ $amount = $this->coercePercent($args[1]);
+
+ $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
+ $color[4] = min(1, max(0, $color[4]));
+ return $color;
+ }
+
+ protected static $lib_fade_in = array("color", "amount");
+ protected function lib_fade_in($args) {
+ return $this->lib_opacify($args);
+ }
+
+ // decreases opacity by amount
+ protected static $lib_transparentize = array("color", "amount");
+ protected function lib_transparentize($args) {
+ $color = $this->assertColor($args[0]);
+ $amount = $this->coercePercent($args[1]);
+
+ $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
+ $color[4] = min(1, max(0, $color[4]));
+ return $color;
+ }
+
+ protected static $lib_fade_out = array("color", "amount");
+ protected function lib_fade_out($args) {
+ return $this->lib_transparentize($args);
+ }
+
+ protected static $lib_unquote = array("string");
+ protected function lib_unquote($args) {
+ $str = $args[0];
+ if ($str[0] == "string") $str[1] = "";
+ return $str;
+ }
+
+ protected static $lib_quote = array("string");
+ protected function lib_quote($args) {
+ $value = $args[0];
+ if ($value[0] == "string" && !empty($value[1]))
+ return $value;
+ return array("string", '"', array($value));
+ }
+
+ protected static $lib_percentage = array("value");
+ protected function lib_percentage($args) {
+ return array("number",
+ $this->coercePercent($args[0]) * 100,
+ "%");
+ }
+
+ protected static $lib_round = array("value");
+ protected function lib_round($args) {
+ $num = $args[0];
+ $num[1] = round($num[1]);
+ return $num;
+ }
+
+ protected static $lib_floor = array("value");
+ protected function lib_floor($args) {
+ $num = $args[0];
+ $num[1] = floor($num[1]);
+ return $num;
+ }
+
+ protected static $lib_ceil = array("value");
+ protected function lib_ceil($args) {
+ $num = $args[0];
+ $num[1] = ceil($num[1]);
+ return $num;
+ }
+
+ protected static $lib_abs = array("value");
+ protected function lib_abs($args) {
+ $num = $args[0];
+ $num[1] = abs($num[1]);
+ return $num;
+ }
+
+ protected function lib_min($args) {
+ $numbers = $this->getNormalizedNumbers($args);
+ $min = null;
+ foreach ($numbers as $key => $number) {
+ if (null === $min || $number[1] <= $min[1]) {
+ $min = array($key, $number[1]);
+ }
+ }
+
+ return $args[$min[0]];
+ }
+
+ protected function lib_max($args) {
+ $numbers = $this->getNormalizedNumbers($args);
+ $max = null;
+ foreach ($numbers as $key => $number) {
+ if (null === $max || $number[1] >= $max[1]) {
+ $max = array($key, $number[1]);
+ }
+ }
+
+ return $args[$max[0]];
+ }
+
+ protected function getNormalizedNumbers($args) {
+ $unit = null;
+ $originalUnit = null;
+ $numbers = array();
+ foreach ($args as $key => $item) {
+ if ('number' != $item[0]) {
+ $this->throwError("%s is not a number", $item[0]);
+ }
+ $number = $this->normalizeNumber($item);
+
+ if (null === $unit) {
+ $unit = $number[2];
+ $originalUnit = $item[2];
+ } elseif ($unit !== $number[2]) {
+ $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
+ }
+
+ $numbers[$key] = $number;
+ }
+
+ return $numbers;
+ }
+
+ protected static $lib_length = array("list");
+ protected function lib_length($args) {
+ $list = $this->coerceList($args[0]);
+ return count($list[2]);
+ }
+
+ protected static $lib_nth = array("list", "n");
+ protected function lib_nth($args) {
+ $list = $this->coerceList($args[0]);
+ $n = $this->assertNumber($args[1]) - 1;
+ return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
+ }
+
+ protected function listSeparatorForJoin($list1, $sep) {
+ if (is_null($sep)) return $list1[1];
+ switch ($this->compileValue($sep)) {
+ case "comma":
+ return ",";
+ case "space":
+ return "";
+ default:
+ return $list1[1];
+ }
+ }
+
+ protected static $lib_join = array("list1", "list2", "separator");
+ protected function lib_join($args) {
+ list($list1, $list2, $sep) = $args;
+ $list1 = $this->coerceList($list1, " ");
+ $list2 = $this->coerceList($list2, " ");
+ $sep = $this->listSeparatorForJoin($list1, $sep);
+ return array("list", $sep, array_merge($list1[2], $list2[2]));
+ }
+
+ protected static $lib_append = array("list", "val", "separator");
+ protected function lib_append($args) {
+ list($list1, $value, $sep) = $args;
+ $list1 = $this->coerceList($list1, " ");
+ $sep = $this->listSeparatorForJoin($list1, $sep);
+ return array("list", $sep, array_merge($list1[2], array($value)));
+ }
+
+ protected function lib_zip($args) {
+ foreach ($args as $arg) {
+ $this->assertList($arg);
+ }
+
+ $lists = array();
+ $firstList = array_shift($args);
+ foreach ($firstList[2] as $key => $item) {
+ $list = array("list", "", array($item));
+ foreach ($args as $arg) {
+ if (isset($arg[2][$key])) {
+ $list[2][] = $arg[2][$key];
+ } else {
+ break 2;
+ }
+ }
+ $lists[] = $list;
+ }
+
+ return array("list", ",", $lists);
+ }
+
+ protected static $lib_type_of = array("value");
+ protected function lib_type_of($args) {
+ $value = $args[0];
+ switch ($value[0]) {
+ case "keyword":
+ if ($value == self::$true || $value == self::$false) {
+ return "bool";
+ }
+
+ if ($this->coerceColor($value)) {
+ return "color";
+ }
+
+ return "string";
+ default:
+ return $value[0];
+ }
+ }
+
+ protected static $lib_unit = array("number");
+ protected function lib_unit($args) {
+ $num = $args[0];
+ if ($num[0] == "number") {
+ return array("string", '"', array($num[2]));
+ }
+ return "";
+ }
+
+ protected static $lib_unitless = array("number");
+ protected function lib_unitless($args) {
+ $value = $args[0];
+ return $value[0] == "number" && empty($value[2]);
+ }
+
+ protected static $lib_comparable = array("number-1", "number-2");
+ protected function lib_comparable($args) {
+ list($number1, $number2) = $args;
+ if (!isset($number1[0]) || $number1[0] != "number" || !isset($number2[0]) || $number2[0] != "number") {
+ $this->throwError('Invalid argument(s) for "comparable"');
+ }
+
+ $number1 = $this->normalizeNumber($number1);
+ $number2 = $this->normalizeNumber($number2);
+
+ return $number1[2] == $number2[2] || $number1[2] == "" || $number2[2] == "";
+ }
+
+ /**
+ * Workaround IE7's content counter bug.
+ *
+ * @param array $args
+ */
+ protected function lib_counter($args) {
+ $list = array_map(array($this, 'compileValue'), $args);
+ return array('string', '', array('counter(' . implode(',', $list) . ')'));
+ }
+
+ public function throwError($msg = null) {
+ if (func_num_args() > 1) {
+ $msg = call_user_func_array("sprintf", func_get_args());
+ }
+
+ if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
+ $this->sourceParser->throwParseError($msg, $this->sourcePos);
+ }
+
+ throw new Exception($msg);
+ }
+
+ /**
+ * CSS Colors
+ *
+ * @see http://www.w3.org/TR/css3-color
+ */
+ static protected $cssColors = array(
+ 'aliceblue' => '240,248,255',
+ 'antiquewhite' => '250,235,215',
+ 'aqua' => '0,255,255',
+ 'aquamarine' => '127,255,212',
+ 'azure' => '240,255,255',
+ 'beige' => '245,245,220',
+ 'bisque' => '255,228,196',
+ 'black' => '0,0,0',
+ 'blanchedalmond' => '255,235,205',
+ 'blue' => '0,0,255',
+ 'blueviolet' => '138,43,226',
+ 'brown' => '165,42,42',
+ 'burlywood' => '222,184,135',
+ 'cadetblue' => '95,158,160',
+ 'chartreuse' => '127,255,0',
+ 'chocolate' => '210,105,30',
+ 'coral' => '255,127,80',
+ 'cornflowerblue' => '100,149,237',
+ 'cornsilk' => '255,248,220',
+ 'crimson' => '220,20,60',
+ 'cyan' => '0,255,255',
+ 'darkblue' => '0,0,139',
+ 'darkcyan' => '0,139,139',
+ 'darkgoldenrod' => '184,134,11',
+ 'darkgray' => '169,169,169',
+ 'darkgreen' => '0,100,0',
+ 'darkgrey' => '169,169,169',
+ 'darkkhaki' => '189,183,107',
+ 'darkmagenta' => '139,0,139',
+ 'darkolivegreen' => '85,107,47',
+ 'darkorange' => '255,140,0',
+ 'darkorchid' => '153,50,204',
+ 'darkred' => '139,0,0',
+ 'darksalmon' => '233,150,122',
+ 'darkseagreen' => '143,188,143',
+ 'darkslateblue' => '72,61,139',
+ 'darkslategray' => '47,79,79',
+ 'darkslategrey' => '47,79,79',
+ 'darkturquoise' => '0,206,209',
+ 'darkviolet' => '148,0,211',
+ 'deeppink' => '255,20,147',
+ 'deepskyblue' => '0,191,255',
+ 'dimgray' => '105,105,105',
+ 'dimgrey' => '105,105,105',
+ 'dodgerblue' => '30,144,255',
+ 'firebrick' => '178,34,34',
+ 'floralwhite' => '255,250,240',
+ 'forestgreen' => '34,139,34',
+ 'fuchsia' => '255,0,255',
+ 'gainsboro' => '220,220,220',
+ 'ghostwhite' => '248,248,255',
+ 'gold' => '255,215,0',
+ 'goldenrod' => '218,165,32',
+ 'gray' => '128,128,128',
+ 'green' => '0,128,0',
+ 'greenyellow' => '173,255,47',
+ 'grey' => '128,128,128',
+ 'honeydew' => '240,255,240',
+ 'hotpink' => '255,105,180',
+ 'indianred' => '205,92,92',
+ 'indigo' => '75,0,130',
+ 'ivory' => '255,255,240',
+ 'khaki' => '240,230,140',
+ 'lavender' => '230,230,250',
+ 'lavenderblush' => '255,240,245',
+ 'lawngreen' => '124,252,0',
+ 'lemonchiffon' => '255,250,205',
+ 'lightblue' => '173,216,230',
+ 'lightcoral' => '240,128,128',
+ 'lightcyan' => '224,255,255',
+ 'lightgoldenrodyellow' => '250,250,210',
+ 'lightgray' => '211,211,211',
+ 'lightgreen' => '144,238,144',
+ 'lightgrey' => '211,211,211',
+ 'lightpink' => '255,182,193',
+ 'lightsalmon' => '255,160,122',
+ 'lightseagreen' => '32,178,170',
+ 'lightskyblue' => '135,206,250',
+ 'lightslategray' => '119,136,153',
+ 'lightslategrey' => '119,136,153',
+ 'lightsteelblue' => '176,196,222',
+ 'lightyellow' => '255,255,224',
+ 'lime' => '0,255,0',
+ 'limegreen' => '50,205,50',
+ 'linen' => '250,240,230',
+ 'magenta' => '255,0,255',
+ 'maroon' => '128,0,0',
+ 'mediumaquamarine' => '102,205,170',
+ 'mediumblue' => '0,0,205',
+ 'mediumorchid' => '186,85,211',
+ 'mediumpurple' => '147,112,219',
+ 'mediumseagreen' => '60,179,113',
+ 'mediumslateblue' => '123,104,238',
+ 'mediumspringgreen' => '0,250,154',
+ 'mediumturquoise' => '72,209,204',
+ 'mediumvioletred' => '199,21,133',
+ 'midnightblue' => '25,25,112',
+ 'mintcream' => '245,255,250',
+ 'mistyrose' => '255,228,225',
+ 'moccasin' => '255,228,181',
+ 'navajowhite' => '255,222,173',
+ 'navy' => '0,0,128',
+ 'oldlace' => '253,245,230',
+ 'olive' => '128,128,0',
+ 'olivedrab' => '107,142,35',
+ 'orange' => '255,165,0',
+ 'orangered' => '255,69,0',
+ 'orchid' => '218,112,214',
+ 'palegoldenrod' => '238,232,170',
+ 'palegreen' => '152,251,152',
+ 'paleturquoise' => '175,238,238',
+ 'palevioletred' => '219,112,147',
+ 'papayawhip' => '255,239,213',
+ 'peachpuff' => '255,218,185',
+ 'peru' => '205,133,63',
+ 'pink' => '255,192,203',
+ 'plum' => '221,160,221',
+ 'powderblue' => '176,224,230',
+ 'purple' => '128,0,128',
+ 'red' => '255,0,0',
+ 'rosybrown' => '188,143,143',
+ 'royalblue' => '65,105,225',
+ 'saddlebrown' => '139,69,19',
+ 'salmon' => '250,128,114',
+ 'sandybrown' => '244,164,96',
+ 'seagreen' => '46,139,87',
+ 'seashell' => '255,245,238',
+ 'sienna' => '160,82,45',
+ 'silver' => '192,192,192',
+ 'skyblue' => '135,206,235',
+ 'slateblue' => '106,90,205',
+ 'slategray' => '112,128,144',
+ 'slategrey' => '112,128,144',
+ 'snow' => '255,250,250',
+ 'springgreen' => '0,255,127',
+ 'steelblue' => '70,130,180',
+ 'tan' => '210,180,140',
+ 'teal' => '0,128,128',
+ 'thistle' => '216,191,216',
+ 'tomato' => '255,99,71',
+ 'transparent' => '0,0,0,0',
+ 'turquoise' => '64,224,208',
+ 'violet' => '238,130,238',
+ 'wheat' => '245,222,179',
+ 'white' => '255,255,255',
+ 'whitesmoke' => '245,245,245',
+ 'yellow' => '255,255,0',
+ 'yellowgreen' => '154,205,50'
+ );
+}
+
+/**
+ * SCSS parser
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class scss_parser {
+ static protected $precedence = array(
+ "or" => 0,
+ "and" => 1,
+
+ '==' => 2,
+ '!=' => 2,
+ '<=' => 2,
+ '>=' => 2,
+ '=' => 2,
+ '<' => 3,
+ '>' => 2,
+
+ '+' => 3,
+ '-' => 3,
+ '*' => 4,
+ '/' => 4,
+ '%' => 4,
+ );
+
+ static protected $operators = array("+", "-", "*", "/", "%",
+ "==", "!=", "<=", ">=", "<", ">", "and", "or");
+
+ static protected $operatorStr;
+ static protected $whitePattern;
+ static protected $commentMulti;
+
+ static protected $commentSingle = "//";
+ static protected $commentMultiLeft = "/*";
+ static protected $commentMultiRight = "*/";
+
+ public function __construct($sourceName = null, $rootParser = true) {
+ $this->sourceName = $sourceName;
+ $this->rootParser = $rootParser;
+
+ if (empty(self::$operatorStr)) {
+ self::$operatorStr = $this->makeOperatorStr(self::$operators);
+
+ $commentSingle = $this->preg_quote(self::$commentSingle);
+ $commentMultiLeft = $this->preg_quote(self::$commentMultiLeft);
+ $commentMultiRight = $this->preg_quote(self::$commentMultiRight);
+ self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
+ self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
+ }
+ }
+
+ static protected function makeOperatorStr($operators) {
+ return '('.implode('|', array_map(array('scss_parser','preg_quote'),
+ $operators)).')';
+ }
+
+ public function parse($buffer) {
+ $this->count = 0;
+ $this->env = null;
+ $this->inParens = false;
+ $this->pushBlock(null); // root block
+ $this->eatWhiteDefault = true;
+ $this->insertComments = true;
+
+ $this->buffer = $buffer;
+
+ $this->whitespace();
+ while (false !== $this->parseChunk());
+
+ if ($this->count != strlen($this->buffer))
+ $this->throwParseError();
+
+ if (!empty($this->env->parent)) {
+ $this->throwParseError("unclosed block");
+ }
+
+ $this->env->isRoot = true;
+ return $this->env;
+ }
+
+ /**
+ * Parse a single chunk off the head of the buffer and append it to the
+ * current parse environment.
+ *
+ * Returns false when the buffer is empty, or when there is an error.
+ *
+ * This function is called repeatedly until the entire document is
+ * parsed.
+ *
+ * This parser is most similar to a recursive descent parser. Single
+ * functions represent discrete grammatical rules for the language, and
+ * they are able to capture the text that represents those rules.
+ *
+ * Consider the function scssc::keyword(). (All parse functions are
+ * structured the same.)
+ *
+ * The function takes a single reference argument. When calling the
+ * function it will attempt to match a keyword on the head of the buffer.
+ * If it is successful, it will place the keyword in the referenced
+ * argument, advance the position in the buffer, and return true. If it
+ * fails then it won't advance the buffer and it will return false.
+ *
+ * All of these parse functions are powered by scssc::match(), which behaves
+ * the same way, but takes a literal regular expression. Sometimes it is
+ * more convenient to use match instead of creating a new function.
+ *
+ * Because of the format of the functions, to parse an entire string of
+ * grammatical rules, you can chain them together using &&.
+ *
+ * But, if some of the rules in the chain succeed before one fails, then
+ * the buffer position will be left at an invalid state. In order to
+ * avoid this, scssc::seek() is used to remember and set buffer positions.
+ *
+ * Before parsing a chain, use $s = $this->seek() to remember the current
+ * position into $s. Then if a chain fails, use $this->seek($s) to
+ * go back where we started.
+ *
+ * @return boolean
+ */
+ protected function parseChunk() {
+ $s = $this->seek();
+
+ // the directives
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
+ if ($this->literal("@media") && $this->mediaQueryList($mediaQueryList) && $this->literal("{")) {
+ $media = $this->pushSpecialBlock("media");
+ $media->queryList = $mediaQueryList[2];
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@mixin") &&
+ $this->keyword($mixinName) &&
+ ($this->argumentDef($args) || true) &&
+ $this->literal("{"))
+ {
+ $mixin = $this->pushSpecialBlock("mixin");
+ $mixin->name = $mixinName;
+ $mixin->args = $args;
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@include") &&
+ $this->keyword($mixinName) &&
+ ($this->literal("(") &&
+ ($this->argValues($argValues) || true) &&
+ $this->literal(")") || true) &&
+ ($this->end() ||
+ $this->literal("{") && $hasBlock = true))
+ {
+ $child = array("include",
+ $mixinName, isset($argValues) ? $argValues : null, null);
+
+ if (!empty($hasBlock)) {
+ $include = $this->pushSpecialBlock("include");
+ $include->child = $child;
+ } else {
+ $this->append($child, $s);
+ }
+
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@import") &&
+ $this->valueList($importPath) &&
+ $this->end())
+ {
+ $this->append(array("import", $importPath), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@extend") &&
+ $this->selectors($selector) &&
+ $this->end())
+ {
+ $this->append(array("extend", $selector), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@function") &&
+ $this->keyword($fnName) &&
+ $this->argumentDef($args) &&
+ $this->literal("{"))
+ {
+ $func = $this->pushSpecialBlock("function");
+ $func->name = $fnName;
+ $func->args = $args;
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) {
+ $this->append(array("return", $retVal), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@each") &&
+ $this->variable($varName) &&
+ $this->literal("in") &&
+ $this->valueList($list) &&
+ $this->literal("{"))
+ {
+ $each = $this->pushSpecialBlock("each");
+ $each->var = $varName[1];
+ $each->list = $list;
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@while") &&
+ $this->expression($cond) &&
+ $this->literal("{"))
+ {
+ $while = $this->pushSpecialBlock("while");
+ $while->cond = $cond;
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@for") &&
+ $this->variable($varName) &&
+ $this->literal("from") &&
+ $this->expression($start) &&
+ ($this->literal("through") ||
+ ($forUntil = true && $this->literal("to"))) &&
+ $this->expression($end) &&
+ $this->literal("{"))
+ {
+ $for = $this->pushSpecialBlock("for");
+ $for->var = $varName[1];
+ $for->start = $start;
+ $for->end = $end;
+ $for->until = isset($forUntil);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@if") && $this->valueList($cond) && $this->literal("{")) {
+ $if = $this->pushSpecialBlock("if");
+ $if->cond = $cond;
+ $if->cases = array();
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if (($this->literal("@debug") || $this->literal("@warn")) &&
+ $this->valueList($value) &&
+ $this->end()) {
+ $this->append(array("debug", $value, $s), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("@content") && $this->end()) {
+ $this->append(array("mixin_content"), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ $last = $this->last();
+ if (!is_null($last) && $last[0] == "if") {
+ list(, $if) = $last;
+ if ($this->literal("@else")) {
+ if ($this->literal("{")) {
+ $else = $this->pushSpecialBlock("else");
+ } elseif ($this->literal("if") && $this->valueList($cond) && $this->literal("{")) {
+ $else = $this->pushSpecialBlock("elseif");
+ $else->cond = $cond;
+ }
+
+ if (isset($else)) {
+ $else->dontAppend = true;
+ $if->cases[] = $else;
+ return true;
+ }
+ }
+
+ $this->seek($s);
+ }
+
+ if ($this->literal("@charset") &&
+ $this->valueList($charset) && $this->end())
+ {
+ $this->append(array("charset", $charset), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // doesn't match built in directive, do generic one
+ if ($this->literal("@", false) && $this->keyword($dirName) &&
+ ($this->openString("{", $dirValue) || true) &&
+ $this->literal("{"))
+ {
+ $directive = $this->pushSpecialBlock("directive");
+ $directive->name = $dirName;
+ if (isset($dirValue)) $directive->value = $dirValue;
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ // property shortcut
+ // captures most properties before having to parse a selector
+ if ($this->keyword($name, false) &&
+ $this->literal(": ") &&
+ $this->valueList($value) &&
+ $this->end())
+ {
+ $name = array("string", "", array($name));
+ $this->append(array("assign", $name, $value), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // variable assigns
+ if ($this->variable($name) &&
+ $this->literal(":") &&
+ $this->valueList($value) && $this->end())
+ {
+ // check for !default
+ $defaultVar = $value[0] == "list" && $this->stripDefault($value);
+ $this->append(array("assign", $name, $value, $defaultVar), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // misc
+ if ($this->literal("-->")) {
+ return true;
+ }
+
+ // opening css block
+ $oldComments = $this->insertComments;
+ $this->insertComments = false;
+ if ($this->selectors($selectors) && $this->literal("{")) {
+ $this->pushBlock($selectors);
+ $this->insertComments = $oldComments;
+ return true;
+ } else {
+ $this->seek($s);
+ }
+ $this->insertComments = $oldComments;
+
+ // property assign, or nested assign
+ if ($this->propertyName($name) && $this->literal(":")) {
+ $foundSomething = false;
+ if ($this->valueList($value)) {
+ $this->append(array("assign", $name, $value), $s);
+ $foundSomething = true;
+ }
+
+ if ($this->literal("{")) {
+ $propBlock = $this->pushSpecialBlock("nestedprop");
+ $propBlock->prefix = $name;
+ $foundSomething = true;
+ } elseif ($foundSomething) {
+ $foundSomething = $this->end();
+ }
+
+ if ($foundSomething) {
+ return true;
+ }
+
+ $this->seek($s);
+ } else {
+ $this->seek($s);
+ }
+
+ // closing a block
+ if ($this->literal("}")) {
+ $block = $this->popBlock();
+ if (isset($block->type) && $block->type == "include") {
+ $include = $block->child;
+ unset($block->child);
+ $include[3] = $block;
+ $this->append($include, $s);
+ } elseif (empty($block->dontAppend)) {
+ $type = isset($block->type) ? $block->type : "block";
+ $this->append(array($type, $block), $s);
+ }
+ return true;
+ }
+
+ // extra stuff
+ if ($this->literal(";") ||
+ $this->literal("<!--"))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function stripDefault(&$value) {
+ $def = end($value[2]);
+ if ($def[0] == "keyword" && $def[1] == "!default") {
+ array_pop($value[2]);
+ $value = $this->flattenList($value);
+ return true;
+ }
+
+ if ($def[0] == "list") {
+ return $this->stripDefault($value[2][count($value[2]) - 1]);
+ }
+
+ return false;
+ }
+
+ protected function literal($what, $eatWhitespace = null) {
+ if (is_null($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
+
+ // shortcut on single letter
+ if (!isset($what[1]) && isset($this->buffer[$this->count])) {
+ if ($this->buffer[$this->count] == $what) {
+ if (!$eatWhitespace) {
+ $this->count++;
+ return true;
+ }
+ // goes below...
+ } else {
+ return false;
+ }
+ }
+
+ return $this->match($this->preg_quote($what), $m, $eatWhitespace);
+ }
+
+ // tree builders
+
+ protected function pushBlock($selectors) {
+ $b = new stdClass;
+ $b->parent = $this->env; // not sure if we need this yet
+
+ $b->selectors = $selectors;
+ $b->children = array();
+
+ $this->env = $b;
+ return $b;
+ }
+
+ protected function pushSpecialBlock($type) {
+ $block = $this->pushBlock(null);
+ $block->type = $type;
+ return $block;
+ }
+
+ protected function popBlock() {
+ if (empty($this->env->parent)) {
+ $this->throwParseError("unexpected }");
+ }
+
+ $old = $this->env;
+ $this->env = $this->env->parent;
+ unset($old->parent);
+ return $old;
+ }
+
+ protected function append($statement, $pos=null) {
+ if ($pos !== null) {
+ $statement[-1] = $pos;
+ if (!$this->rootParser) $statement[-2] = $this;
+ }
+ $this->env->children[] = $statement;
+ }
+
+ // last child that was appended
+ protected function last() {
+ $i = count($this->env->children) - 1;
+ if (isset($this->env->children[$i]))
+ return $this->env->children[$i];
+ }
+
+ // high level parsers (they return parts of ast)
+
+ protected function mediaQueryList(&$out) {
+ return $this->genericList($out, "mediaQuery", ",", false);
+ }
+
+ protected function mediaQuery(&$out) {
+ $s = $this->seek();
+
+ $expressions = null;
+ $parts = array();
+
+ if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->mixedKeyword($mediaType)) {
+ $prop = array("mediaType");
+ if (isset($only)) $prop[] = array("keyword", "only");
+ if (isset($not)) $prop[] = array("keyword", "not");
+ $media = array("list", "", array());
+ foreach ((array)$mediaType as $type) {
+ if (is_array($type)) {
+ $media[2][] = $type;
+ } else {
+ $media[2][] = array("keyword", $type);
+ }
+ }
+ $prop[] = $media;
+ $parts[] = $prop;
+ }
+
+ if (empty($parts) || $this->literal("and")) {
+ $this->genericList($expressions, "mediaExpression", "and", false);
+ if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
+ }
+
+ $out = $parts;
+ return true;
+ }
+
+ protected function mediaExpression(&$out) {
+ $s = $this->seek();
+ $value = null;
+ if ($this->literal("(") &&
+ $this->expression($feature) &&
+ ($this->literal(":") && $this->expression($value) || true) &&
+ $this->literal(")"))
+ {
+ $out = array("mediaExp", $feature);
+ if ($value) $out[] = $value;
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ protected function argValues(&$out) {
+ if ($this->genericList($list, "argValue", ",", false)) {
+ $out = $list[2];
+ return true;
+ }
+ return false;
+ }
+
+ protected function argValue(&$out) {
+ $s = $this->seek();
+
+ $keyword = null;
+ if (!$this->variable($keyword) || !$this->literal(":")) {
+ $this->seek($s);
+ $keyword = null;
+ }
+
+ if ($this->genericList($value, "expression")) {
+ $out = array($keyword, $value, false);
+ $s = $this->seek();
+ if ($this->literal("...")) {
+ $out[2] = true;
+ } else {
+ $this->seek($s);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+
+ protected function valueList(&$out) {
+ return $this->genericList($out, "spaceList", ",");
+ }
+
+ protected function spaceList(&$out) {
+ return $this->genericList($out, "expression");
+ }
+
+ protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
+ $s = $this->seek();
+ $items = array();
+ while ($this->$parseItem($value)) {
+ $items[] = $value;
+ if ($delim) {
+ if (!$this->literal($delim)) break;
+ }
+ }
+
+ if (count($items) == 0) {
+ $this->seek($s);
+ return false;
+ }
+
+ if ($flatten && count($items) == 1) {
+ $out = $items[0];
+ } else {
+ $out = array("list", $delim, $items);
+ }
+
+ return true;
+ }
+
+ protected function expression(&$out) {
+ $s = $this->seek();
+
+ if ($this->literal("(")) {
+ if ($this->literal(")")) {
+ $out = array("list", "", array());
+ return true;
+ }
+
+ if ($this->valueList($out) && $this->literal(')') && $out[0] == "list") {
+ return true;
+ }
+
+ $this->seek($s);
+ }
+
+ if ($this->value($lhs)) {
+ $out = $this->expHelper($lhs, 0);
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function expHelper($lhs, $minP) {
+ $opstr = self::$operatorStr;
+
+ $ss = $this->seek();
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+ while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) {
+ $whiteAfter = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+
+ $op = $m[1];
+
+ // don't turn negative numbers into expressions
+ if ($op == "-" && $whiteBefore) {
+ if (!$whiteAfter) break;
+ }
+
+ if (!$this->value($rhs)) break;
+
+ // peek and see if rhs belongs to next operator
+ if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
+ $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
+ }
+
+ $lhs = array("exp", $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
+ $ss = $this->seek();
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+ }
+
+ $this->seek($ss);
+ return $lhs;
+ }
+
+ protected function value(&$out) {
+ $s = $this->seek();
+
+ if ($this->literal("not", false) && $this->whitespace() && $this->value($inner)) {
+ $out = array("unary", "not", $inner, $this->inParens);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal("+") && $this->value($inner)) {
+ $out = array("unary", "+", $inner, $this->inParens);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // negation
+ if ($this->literal("-", false) &&
+ ($this->variable($inner) ||
+ $this->unit($inner) ||
+ $this->parenValue($inner)))
+ {
+ $out = array("unary", "-", $inner, $this->inParens);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->parenValue($out)) return true;
+ if ($this->interpolation($out)) return true;
+ if ($this->variable($out)) return true;
+ if ($this->color($out)) return true;
+ if ($this->unit($out)) return true;
+ if ($this->string($out)) return true;
+ if ($this->func($out)) return true;
+ if ($this->progid($out)) return true;
+
+ if ($this->keyword($keyword)) {
+ if ($keyword == "null") {
+ $out = array("null");
+ } else {
+ $out = array("keyword", $keyword);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ // value wrappen in parentheses
+ protected function parenValue(&$out) {
+ $s = $this->seek();
+
+ $inParens = $this->inParens;
+ if ($this->literal("(") &&
+ ($this->inParens = true) && $this->expression($exp) &&
+ $this->literal(")"))
+ {
+ $out = $exp;
+ $this->inParens = $inParens;
+ return true;
+ } else {
+ $this->inParens = $inParens;
+ $this->seek($s);
+ }
+
+ return false;
+ }
+
+ protected function progid(&$out) {
+ $s = $this->seek();
+ if ($this->literal("progid:", false) &&
+ $this->openString("(", $fn) &&
+ $this->literal("("))
+ {
+ $this->openString(")", $args, "(");
+ if ($this->literal(")")) {
+ $out = array("string", "", array(
+ "progid:", $fn, "(", $args, ")"
+ ));
+ return true;
+ }
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ protected function func(&$func) {
+ $s = $this->seek();
+
+ if ($this->keyword($name, false) &&
+ $this->literal("("))
+ {
+ if ($name == "alpha" && $this->argumentList($args)) {
+ $func = array("function", $name, array("string", "", $args));
+ return true;
+ }
+
+ if ($name != "expression" && !preg_match("/^(-[a-z]+-)?calc$/", $name)) {
+ $ss = $this->seek();
+ if ($this->argValues($args) && $this->literal(")")) {
+ $func = array("fncall", $name, $args);
+ return true;
+ }
+ $this->seek($ss);
+ }
+
+ if (($this->openString(")", $str, "(") || true ) &&
+ $this->literal(")"))
+ {
+ $args = array();
+ if (!empty($str)) {
+ $args[] = array(null, array("string", "", array($str)));
+ }
+
+ $func = array("fncall", $name, $args);
+ return true;
+ }
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ protected function argumentList(&$out) {
+ $s = $this->seek();
+ $this->literal("(");
+
+ $args = array();
+ while ($this->keyword($var)) {
+ $ss = $this->seek();
+
+ if ($this->literal("=") && $this->expression($exp)) {
+ $args[] = array("string", "", array($var."="));
+ $arg = $exp;
+ } else {
+ break;
+ }
+
+ $args[] = $arg;
+
+ if (!$this->literal(",")) break;
+
+ $args[] = array("string", "", array(", "));
+ }
+
+ if (!$this->literal(")") || !count($args)) {
+ $this->seek($s);
+ return false;
+ }
+
+ $out = $args;
+ return true;
+ }
+
+ protected function argumentDef(&$out) {
+ $s = $this->seek();
+ $this->literal("(");
+
+ $args = array();
+ while ($this->variable($var)) {
+ $arg = array($var[1], null, false);
+
+ $ss = $this->seek();
+ if ($this->literal(":") && $this->genericList($defaultVal, "expression")) {
+ $arg[1] = $defaultVal;
+ } else {
+ $this->seek($ss);
+ }
+
+ $ss = $this->seek();
+ if ($this->literal("...")) {
+ $sss = $this->seek();
+ if (!$this->literal(")")) {
+ $this->throwParseError("... has to be after the final argument");
+ }
+ $arg[2] = true;
+ $this->seek($sss);
+ } else {
+ $this->seek($ss);
+ }
+
+ $args[] = $arg;
+ if (!$this->literal(",")) break;
+ }
+
+ if (!$this->literal(")")) {
+ $this->seek($s);
+ return false;
+ }
+
+ $out = $args;
+ return true;
+ }
+
+ protected function color(&$out) {
+ $color = array('color');
+
+ if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
+ if (isset($m[3])) {
+ $num = $m[3];
+ $width = 16;
+ } else {
+ $num = $m[2];
+ $width = 256;
+ }
+
+ $num = hexdec($num);
+ foreach (array(3,2,1) as $i) {
+ $t = $num % $width;
+ $num /= $width;
+
+ $color[$i] = $t * (256/$width) + $t * floor(16/$width);
+ }
+
+ $out = $color;
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function unit(&$unit) {
+ if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
+ $unit = array("number", $m[1], empty($m[3]) ? "" : $m[3]);
+ return true;
+ }
+ return false;
+ }
+
+ protected function string(&$out) {
+ $s = $this->seek();
+ if ($this->literal('"', false)) {
+ $delim = '"';
+ } elseif ($this->literal("'", false)) {
+ $delim = "'";
+ } else {
+ return false;
+ }
+
+ $content = array();
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while ($this->matchString($m, $delim)) {
+ $content[] = $m[1];
+ if ($m[2] == "#{") {
+ $this->count -= strlen($m[2]);
+ if ($this->interpolation($inter, false)) {
+ $content[] = $inter;
+ } else {
+ $this->count += strlen($m[2]);
+ $content[] = "#{"; // ignore it
+ }
+ } elseif ($m[2] == '\\') {
+ $content[] = $m[2];
+ if ($this->literal($delim, false)) {
+ $content[] = $delim;
+ }
+ } else {
+ $this->count -= strlen($delim);
+ break; // delim
+ }
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if ($this->literal($delim)) {
+ $out = array("string", $delim, $content);
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ protected function mixedKeyword(&$out) {
+ $s = $this->seek();
+
+ $parts = array();
+
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while (true) {
+ if ($this->keyword($key)) {
+ $parts[] = $key;
+ continue;
+ }
+
+ if ($this->interpolation($inter)) {
+ $parts[] = $inter;
+ continue;
+ }
+
+ break;
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if (count($parts) == 0) return false;
+
+ if ($this->eatWhiteDefault) {
+ $this->whitespace();
+ }
+
+ $out = $parts;
+ return true;
+ }
+
+ // an unbounded string stopped by $end
+ protected function openString($end, &$out, $nestingOpen=null) {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ $stop = array("'", '"', "#{", $end);
+ $stop = array_map(array($this, "preg_quote"), $stop);
+ $stop[] = self::$commentMulti;
+
+ $patt = '(.*?)('.implode("|", $stop).')';
+
+ $nestingLevel = 0;
+
+ $content = array();
+ while ($this->match($patt, $m, false)) {
+ if (isset($m[1]) && $m[1] !== '') {
+ $content[] = $m[1];
+ if ($nestingOpen) {
+ $nestingLevel += substr_count($m[1], $nestingOpen);
+ }
+ }
+
+ $tok = $m[2];
+
+ $this->count-= strlen($tok);
+ if ($tok == $end) {
+ if ($nestingLevel == 0) {
+ break;
+ } else {
+ $nestingLevel--;
+ }
+ }
+
+ if (($tok == "'" || $tok == '"') && $this->string($str)) {
+ $content[] = $str;
+ continue;
+ }
+
+ if ($tok == "#{" && $this->interpolation($inter)) {
+ $content[] = $inter;
+ continue;
+ }
+
+ $content[] = $tok;
+ $this->count+= strlen($tok);
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if (count($content) == 0) return false;
+
+ // trim the end
+ if (is_string(end($content))) {
+ $content[count($content) - 1] = rtrim(end($content));
+ }
+
+ $out = array("string", "", $content);
+ return true;
+ }
+
+ // $lookWhite: save information about whitespace before and after
+ protected function interpolation(&$out, $lookWhite=true) {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = true;
+
+ $s = $this->seek();
+ if ($this->literal("#{") && $this->valueList($value) && $this->literal("}", false)) {
+
+ // TODO: don't error if out of bounds
+
+ if ($lookWhite) {
+ $left = preg_match('/\s/', $this->buffer[$s - 1]) ? " " : "";
+ $right = preg_match('/\s/', $this->buffer[$this->count]) ? " ": "";
+ } else {
+ $left = $right = false;
+ }
+
+ $out = array("interpolate", $value, $left, $right);
+ $this->eatWhiteDefault = $oldWhite;
+ if ($this->eatWhiteDefault) $this->whitespace();
+ return true;
+ }
+
+ $this->seek($s);
+ $this->eatWhiteDefault = $oldWhite;
+ return false;
+ }
+
+ // low level parsers
+
+ // returns an array of parts or a string
+ protected function propertyName(&$out) {
+ $s = $this->seek();
+ $parts = array();
+
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while (true) {
+ if ($this->interpolation($inter)) {
+ $parts[] = $inter;
+ } elseif ($this->keyword($text)) {
+ $parts[] = $text;
+ } elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) {
+ // css hacks
+ $parts[] = $m[0];
+ } else {
+ break;
+ }
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+ if (count($parts) == 0) return false;
+
+ // match comment hack
+ if (preg_match(self::$whitePattern,
+ $this->buffer, $m, null, $this->count))
+ {
+ if (!empty($m[0])) {
+ $parts[] = $m[0];
+ $this->count += strlen($m[0]);
+ }
+ }
+
+ $this->whitespace(); // get any extra whitespace
+
+ $out = array("string", "", $parts);
+ return true;
+ }
+
+ // comma separated list of selectors
+ protected function selectors(&$out) {
+ $s = $this->seek();
+ $selectors = array();
+ while ($this->selector($sel)) {
+ $selectors[] = $sel;
+ if (!$this->literal(",")) break;
+ while ($this->literal(",")); // ignore extra
+ }
+
+ if (count($selectors) == 0) {
+ $this->seek($s);
+ return false;
+ }
+
+ $out = $selectors;
+ return true;
+ }
+
+ // whitespace separated list of selectorSingle
+ protected function selector(&$out) {
+ $selector = array();
+
+ while (true) {
+ if ($this->match('[>+~]+', $m)) {
+ $selector[] = array($m[0]);
+ } elseif ($this->selectorSingle($part)) {
+ $selector[] = $part;
+ $this->whitespace();
+ } elseif ($this->match('\/[^\/]+\/', $m)) {
+ $selector[] = array($m[0]);
+ } else {
+ break;
+ }
+
+ }
+
+ if (count($selector) == 0) {
+ return false;
+ }
+
+ $out = $selector;
+ return true;
+ }
+
+ // the parts that make up
+ // div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
+ protected function selectorSingle(&$out) {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ $parts = array();
+
+ if ($this->literal("*", false)) {
+ $parts[] = "*";
+ }
+
+ while (true) {
+ // see if we can stop early
+ if ($this->match("\s*[{,]", $m)) {
+ $this->count--;
+ break;
+ }
+
+ $s = $this->seek();
+ // self
+ if ($this->literal("&", false)) {
+ $parts[] = scssc::$selfSelector;
+ continue;
+ }
+
+ if ($this->literal(".", false)) {
+ $parts[] = ".";
+ continue;
+ }
+
+ if ($this->literal("|", false)) {
+ $parts[] = "|";
+ continue;
+ }
+
+ // for keyframes
+ if ($this->unit($unit)) {
+ $parts[] = $unit;
+ continue;
+ }
+
+ if ($this->keyword($name)) {
+ $parts[] = $name;
+ continue;
+ }
+
+ if ($this->interpolation($inter)) {
+ $parts[] = $inter;
+ continue;
+ }
+
+ if ($this->literal('%', false) && $this->placeholder($placeholder)) {
+ $parts[] = '%';
+ $parts[] = $placeholder;
+ continue;
+ }
+
+ if ($this->literal("#", false)) {
+ $parts[] = "#";
+ continue;
+ }
+
+ // a pseudo selector
+ if ($this->match("::?", $m) && $this->mixedKeyword($nameParts)) {
+ $parts[] = $m[0];
+ foreach ($nameParts as $sub) {
+ $parts[] = $sub;
+ }
+
+ $ss = $this->seek();
+ if ($this->literal("(") &&
+ ($this->openString(")", $str, "(") || true ) &&
+ $this->literal(")"))
+ {
+ $parts[] = "(";
+ if (!empty($str)) $parts[] = $str;
+ $parts[] = ")";
+ } else {
+ $this->seek($ss);
+ }
+
+ continue;
+ } else {
+ $this->seek($s);
+ }
+
+ // attribute selector
+ // TODO: replace with open string?
+ if ($this->literal("[", false)) {
+ $attrParts = array("[");
+ // keyword, string, operator
+ while (true) {
+ if ($this->literal("]", false)) {
+ $this->count--;
+ break; // get out early
+ }
+
+ if ($this->match('\s+', $m)) {
+ $attrParts[] = " ";
+ continue;
+ }
+ if ($this->string($str)) {
+ $attrParts[] = $str;
+ continue;
+ }
+
+ if ($this->keyword($word)) {
+ $attrParts[] = $word;
+ continue;
+ }
+
+ if ($this->interpolation($inter, false)) {
+ $attrParts[] = $inter;
+ continue;
+ }
+
+ // operator, handles attr namespace too
+ if ($this->match('[|-~\$\*\^=]+', $m)) {
+ $attrParts[] = $m[0];
+ continue;
+ }
+
+ break;
+ }
+
+ if ($this->literal("]", false)) {
+ $attrParts[] = "]";
+ foreach ($attrParts as $part) {
+ $parts[] = $part;
+ }
+ continue;
+ }
+ $this->seek($s);
+ // should just break here?
+ }
+
+ break;
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if (count($parts) == 0) return false;
+
+ $out = $parts;
+ return true;
+ }
+
+ protected function variable(&$out) {
+ $s = $this->seek();
+ if ($this->literal("$", false) && $this->keyword($name)) {
+ $out = array("var", $name);
+ return true;
+ }
+ $this->seek($s);
+ return false;
+ }
+
+ protected function keyword(&$word, $eatWhitespace = null) {
+ if ($this->match('([\w_\-\*!"\'\\\\][\w\-_"\'\\\\]*)',
+ $m, $eatWhitespace))
+ {
+ $word = $m[1];
+ return true;
+ }
+ return false;
+ }
+
+ protected function placeholder(&$placeholder) {
+ if ($this->match('([\w\-_]+)', $m)) {
+ $placeholder = $m[1];
+ return true;
+ }
+ return false;
+ }
+
+ // consume an end of statement delimiter
+ protected function end() {
+ if ($this->literal(';')) {
+ return true;
+ } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
+ // if there is end of file or a closing block next then we don't need a ;
+ return true;
+ }
+ return false;
+ }
+
+ // advance counter to next occurrence of $what
+ // $until - don't include $what in advance
+ // $allowNewline, if string, will be used as valid char set
+ protected function to($what, &$out, $until = false, $allowNewline = false) {
+ if (is_string($allowNewline)) {
+ $validChars = $allowNewline;
+ } else {
+ $validChars = $allowNewline ? "." : "[^\n]";
+ }
+ if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false;
+ if ($until) $this->count -= strlen($what); // give back $what
+ $out = $m[1];
+ return true;
+ }
+
+ public function throwParseError($msg = "parse error", $count = null) {
+ $count = is_null($count) ? $this->count : $count;
+
+ $line = $this->getLineNo($count);
+
+ if (!empty($this->sourceName)) {
+ $loc = "$this->sourceName on line $line";
+ } else {
+ $loc = "line: $line";
+ }
+
+ if ($this->peek("(.*?)(\n|$)", $m, $count)) {
+ throw new Exception("$msg: failed at `$m[1]` $loc");
+ } else {
+ throw new Exception("$msg: $loc");
+ }
+ }
+
+ public function getLineNo($pos) {
+ return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
+ }
+
+ /**
+ * Match string looking for either ending delim, escape, or string interpolation
+ *
+ * {@internal This is a workaround for preg_match's 250K string match limit. }}
+ *
+ * @param array $m Matches (passed by reference)
+ * @param string $delim Delimeter
+ *
+ * @return boolean True if match; false otherwise
+ */
+ protected function matchString(&$m, $delim) {
+ $token = null;
+
+ $end = strpos($this->buffer, "\n", $this->count);
+ if ($end === false) {
+ $end = strlen($this->buffer);
+ }
+
+ // look for either ending delim, escape, or string interpolation
+ foreach (array('#{', '\\', $delim) as $lookahead) {
+ $pos = strpos($this->buffer, $lookahead, $this->count);
+ if ($pos !== false && $pos < $end) {
+ $end = $pos;
+ $token = $lookahead;
+ }
+ }
+
+ if (!isset($token)) {
+ return false;
+ }
+
+ $match = substr($this->buffer, $this->count, $end - $this->count);
+ $m = array(
+ $match . $token,
+ $match,
+ $token
+ );
+ $this->count = $end + strlen($token);
+
+ return true;
+ }
+
+ // try to match something on head of buffer
+ protected function match($regex, &$out, $eatWhitespace = null) {
+ if (is_null($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
+
+ $r = '/'.$regex.'/Ais';
+ if (preg_match($r, $this->buffer, $out, null, $this->count)) {
+ $this->count += strlen($out[0]);
+ if ($eatWhitespace) $this->whitespace();
+ return true;
+ }
+ return false;
+ }
+
+ // match some whitespace
+ protected function whitespace() {
+ $gotWhite = false;
+ while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
+ if ($this->insertComments) {
+ if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
+ $this->append(array("comment", $m[1]));
+ $this->commentsSeen[$this->count] = true;
+ }
+ }
+ $this->count += strlen($m[0]);
+ $gotWhite = true;
+ }
+ return $gotWhite;
+ }
+
+ protected function peek($regex, &$out, $from=null) {
+ if (is_null($from)) $from = $this->count;
+
+ $r = '/'.$regex.'/Ais';
+ $result = preg_match($r, $this->buffer, $out, null, $from);
+
+ return $result;
+ }
+
+ protected function seek($where = null) {
+ if ($where === null) return $this->count;
+ else $this->count = $where;
+ return true;
+ }
+
+ static function preg_quote($what) {
+ return preg_quote($what, '/');
+ }
+
+ protected function show() {
+ if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
+ return $m[1];
+ }
+ return "";
+ }
+
+ // turn list of length 1 into value type
+ protected function flattenList($value) {
+ if ($value[0] == "list" && count($value[2]) == 1) {
+ return $this->flattenList($value[2][0]);
+ }
+ return $value;
+ }
+}
+
+/**
+ * SCSS base formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class scss_formatter {
+ public $indentChar = " ";
+
+ public $break = "\n";
+ public $open = " {";
+ public $close = "}";
+ public $tagSeparator = ", ";
+ public $assignSeparator = ": ";
+
+ public function __construct() {
+ $this->indentLevel = 0;
+ }
+
+ public function indentStr($n = 0) {
+ return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
+ }
+
+ public function property($name, $value) {
+ return $name . $this->assignSeparator . $value . ";";
+ }
+
+ protected function block($block) {
+ if (empty($block->lines) && empty($block->children)) return;
+
+ $inner = $pre = $this->indentStr();
+
+ if (!empty($block->selectors)) {
+ echo $pre .
+ implode($this->tagSeparator, $block->selectors) .
+ $this->open . $this->break;
+ $this->indentLevel++;
+ $inner = $this->indentStr();
+ }
+
+ if (!empty($block->lines)) {
+ $glue = $this->break.$inner;
+ echo $inner . implode($glue, $block->lines);
+ if (!empty($block->children)) {
+ echo $this->break;
+ }
+ }
+
+ foreach ($block->children as $child) {
+ $this->block($child);
+ }
+
+ if (!empty($block->selectors)) {
+ $this->indentLevel--;
+ if (empty($block->children)) echo $this->break;
+ echo $pre . $this->close . $this->break;
+ }
+ }
+
+ public function format($block) {
+ ob_start();
+ $this->block($block);
+ $out = ob_get_clean();
+
+ return $out;
+ }
+}
+
+/**
+ * SCSS nested formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class scss_formatter_nested extends scss_formatter {
+ public $close = " }";
+
+ // adjust the depths of all children, depth first
+ public function adjustAllChildren($block) {
+ // flatten empty nested blocks
+ $children = array();
+ foreach ($block->children as $i => $child) {
+ if (empty($child->lines) && empty($child->children)) {
+ if (isset($block->children[$i + 1])) {
+ $block->children[$i + 1]->depth = $child->depth;
+ }
+ continue;
+ }
+ $children[] = $child;
+ }
+
+ $count = count($children);
+ for ($i = 0; $i < $count; $i++) {
+ $depth = $children[$i]->depth;
+ $j = $i + 1;
+ if (isset($children[$j]) && $depth < $children[$j]->depth) {
+ $childDepth = $children[$j]->depth;
+ for (; $j < $count; $j++) {
+ if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
+ $children[$j]->depth = $depth + 1;
+ }
+ }
+ }
+ }
+
+ $block->children = $children;
+
+ // make relative to parent
+ foreach ($block->children as $child) {
+ $this->adjustAllChildren($child);
+ $child->depth = $child->depth - $block->depth;
+ }
+ }
+
+ protected function block($block) {
+ if ($block->type == "root") {
+ $this->adjustAllChildren($block);
+ }
+
+ $inner = $pre = $this->indentStr($block->depth - 1);
+ if (!empty($block->selectors)) {
+ echo $pre .
+ implode($this->tagSeparator, $block->selectors) .
+ $this->open . $this->break;
+ $this->indentLevel++;
+ $inner = $this->indentStr($block->depth - 1);
+ }
+
+ if (!empty($block->lines)) {
+ $glue = $this->break.$inner;
+ echo $inner . implode($glue, $block->lines);
+ if (!empty($block->children)) echo $this->break;
+ }
+
+ foreach ($block->children as $i => $child) {
+ // echo "*** block: ".$block->depth." child: ".$child->depth."\n";
+ $this->block($child);
+ if ($i < count($block->children) - 1) {
+ echo $this->break;
+
+ if (isset($block->children[$i + 1])) {
+ $next = $block->children[$i + 1];
+ if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) {
+ echo $this->break;
+ }
+ }
+ }
+ }
+
+ if (!empty($block->selectors)) {
+ $this->indentLevel--;
+ echo $this->close;
+ }
+
+ if ($block->type == "root") {
+ echo $this->break;
+ }
+ }
+}
+
+/**
+ * SCSS compressed formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class scss_formatter_compressed extends scss_formatter {
+ public $open = "{";
+ public $tagSeparator = ",";
+ public $assignSeparator = ":";
+ public $break = "";
+
+ public function indentStr($n = 0) {
+ return "";
+ }
+}
+
+/**
+ * SCSS server
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class scss_server {
+ /**
+ * Join path components
+ *
+ * @param string $left Path component, left of the directory separator
+ * @param string $right Path component, right of the directory separator
+ *
+ * @return string
+ */
+ protected function join($left, $right) {
+ return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
+ }
+
+ /**
+ * Get name of requested .scss file
+ *
+ * @return string|null
+ */
+ protected function inputName() {
+ switch (true) {
+ case isset($_GET['p']):
+ return $_GET['p'];
+ case isset($_SERVER['PATH_INFO']):
+ return $_SERVER['PATH_INFO'];
+ case isset($_SERVER['DOCUMENT_URI']):
+ return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
+ }
+ }
+
+ /**
+ * Get path to requested .scss file
+ *
+ * @return string
+ */
+ protected function findInput() {
+ if (($input = $this->inputName())
+ && strpos($input, '..') === false
+ && substr($input, -5) === '.scss'
+ ) {
+ $name = $this->join($this->dir, $input);
+
+ if (is_file($name) && is_readable($name)) {
+ return $name;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get path to cached .css file
+ *
+ * @return string
+ */
+ protected function cacheName($fname) {
+ return $this->join($this->cacheDir, md5($fname) . '.css');
+ }
+
+ /**
+ * Get path to cached imports
+ *
+ * @return string
+ */
+ protected function importsCacheName($out) {
+ return $out . '.imports';
+ }
+
+ /**
+ * Determine whether .scss file needs to be re-compiled.
+ *
+ * @param string $in Input path
+ * @param string $out Output path
+ *
+ * @return boolean True if compile required.
+ */
+ protected function needsCompile($in, $out) {
+ if (!is_file($out)) return true;
+
+ $mtime = filemtime($out);
+ if (filemtime($in) > $mtime) return true;
+
+ // look for modified imports
+ $icache = $this->importsCacheName($out);
+ if (is_readable($icache)) {
+ $imports = unserialize(file_get_contents($icache));
+ foreach ($imports as $import) {
+ if (filemtime($import) > $mtime) return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Compile .scss file
+ *
+ * @param string $in Input path (.scss)
+ * @param string $out Output path (.css)
+ *
+ * @return string
+ */
+ protected function compile($in, $out) {
+ $start = microtime(true);
+ $css = $this->scss->compile(file_get_contents($in), $in);
+ $elapsed = round((microtime(true) - $start), 4);
+
+ $v = scssc::$VERSION;
+ $t = date('r');
+ $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
+
+ file_put_contents($out, $css);
+ file_put_contents($this->importsCacheName($out),
+ serialize($this->scss->getParsedFiles()));
+ return $css;
+ }
+
+ /**
+ * Compile requested scss and serve css. Outputs HTTP response.
+ *
+ * @param string $salt Prefix a string to the filename for creating the cache name hash
+ */
+ public function serve($salt = '') {
+ if ($input = $this->findInput()) {
+ $output = $this->cacheName($salt . $input);
+ header('Content-type: text/css');
+
+ if ($this->needsCompile($input, $output)) {
+ try {
+ echo $this->compile($input, $output);
+ } catch (Exception $e) {
+ header('HTTP/1.1 500 Internal Server Error');
+ echo 'Parse error: ' . $e->getMessage() . "\n";
+ }
+ } else {
+ header('X-SCSS-Cache: true');
+ echo file_get_contents($output);
+ }
+
+ return;
+ }
+
+ header('HTTP/1.0 404 Not Found');
+ header('Content-type: text');
+ $v = scssc::$VERSION;
+ echo "/* INPUT NOT FOUND scss $v */\n";
+ }
+
+ /**
+ * Constructor
+ *
+ * @param string $dir Root directory to .scss files
+ * @param string $cacheDir Cache directory
+ * @param \scssc|null $scss SCSS compiler instance
+ */
+ public function __construct($dir, $cacheDir=null, $scss=null) {
+ $this->dir = $dir;
+
+ if (is_null($cacheDir)) {
+ $cacheDir = $this->join($dir, 'scss_cache');
+ }
+
+ $this->cacheDir = $cacheDir;
+ if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true);
+
+ if (is_null($scss)) {
+ $scss = new scssc();
+ $scss->setImportPaths($this->dir);
+ }
+ $this->scss = $scss;
+ }
+
+ /**
+ * Helper method to serve compiled scss
+ *
+ * @param string $path Root path
+ */
+ static public function serveFrom($path) {
+ $server = new self($path);
+ $server->serve();
+ }
+}
diff --git a/plugins/jetpack/modules/custom-css/migrate-to-core.php b/plugins/jetpack/modules/custom-css/migrate-to-core.php
new file mode 100644
index 00000000..26108941
--- /dev/null
+++ b/plugins/jetpack/modules/custom-css/migrate-to-core.php
@@ -0,0 +1,243 @@
+<?php
+/**
+ * Migration from Jetpack Custom CSS to WordPress' Core CSS.
+ *
+ * @since 4.4.2
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Class Jetpack_Custom_CSS_Data_Migration
+ */
+class Jetpack_Custom_CSS_Data_Migration {
+ /**
+ * Set up assorted actions and filters used by this class.
+ */
+ public static function add_hooks() {
+ add_action( 'init', array( __CLASS__, 'register_legacy_post_type' ) );
+ add_action( 'admin_init', array( __CLASS__, 'do_migration' ) );
+
+ include_once( dirname( __FILE__ ) . '/custom-css.php' );
+ if ( ! is_admin() ) {
+ add_action( 'init', array( 'Jetpack_Custom_CSS', 'init' ) );
+ }
+ }
+
+ /**
+ * Do the bulk of the migration.
+ *
+ * @return int|null
+ */
+ public static function do_migration() {
+ Jetpack_Options::update_option( 'custom_css_4.7_migration', true );
+ Jetpack::log( 'custom_css_4.7_migration', 'start' );
+
+ if ( ! post_type_exists( 'safecss' ) ) {
+ self::register_legacy_post_type();
+ }
+
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
+ $core_css_post = wp_get_custom_css_post();
+ $jetpack_css_post = self::get_post();
+
+ if ( ! $jetpack_css_post ) {
+ return;
+ }
+
+ $revisions = self::get_all_revisions();
+
+ // Migrate the settings from revision meta to theme mod.
+ $options = self::get_options( $jetpack_css_post->ID );
+ set_theme_mod( 'jetpack_custom_css', $options );
+
+ if ( empty( $revisions ) || ! is_array( $revisions ) ) {
+ if ( $jetpack_css_post instanceof WP_Post ) {
+ // Feed in the raw, if the current setting is Sass/LESS, it'll filter it inside.
+ kses_remove_filters();
+ wp_update_custom_css_post( $jetpack_css_post->post_content );
+ kses_init();
+ return 1;
+ }
+ return null;
+ }
+
+ $revisions = array_reverse( $revisions );
+ $themes = Jetpack_Custom_CSS_Enhancements::get_themes();
+ $migrated = array();
+
+ foreach ( $revisions as $post_id => $post ) {
+ // Jetpack had stored the theme Name, not the stylesheet directory, for ... reasons.
+ // Get the stylesheet. If null, the theme is no longer available. Skip.
+ $stylesheet = isset( $themes[ $post->post_excerpt ] ) ? $themes[ $post->post_excerpt ] : null;
+ if ( empty( $stylesheet ) ) {
+ continue;
+ }
+
+ $migrated[] = $post->ID;
+ $preprocessor = get_post_meta( $post->ID, 'custom_css_preprocessor', true );
+ $css = $post->post_content;
+ $pre = '';
+
+ // Do a revision by revision parsing.
+ if ( $preprocessor && isset( $preprocessors[ $preprocessor ] ) ) {
+ $pre = $css;
+ $css = call_user_func( $preprocessors[ $preprocessor ]['callback'], $pre );
+ }
+
+ kses_remove_filters();
+ wp_update_custom_css_post( $css, array(
+ 'stylesheet' => $stylesheet,
+ 'preprocessed' => $pre,
+ ) );
+ kses_init();
+ }
+
+ // If we've migrated some CSS for the current theme and there was already something there in the Core dataset ...
+ if ( $core_css_post && $jetpack_css_post ) {
+ $preprocessor = $options['preprocessor'];
+
+ $css = $core_css_post->post_content;
+ $pre = $core_css_post->post_content_filtered;
+ if ( $preprocessor ) {
+ if ( $pre ) {
+ $pre .= "\r\n\r\n/*\r\n\t" . esc_js( __( 'CSS Migrated from Jetpack:', 'jetpack' ) ) . "\r\n*/\r\n\r\n";
+ }
+ $pre .= $jetpack_css_post->post_content;
+
+ $css .= "\r\n\r\n/*\r\n\t" . esc_js( __( 'CSS Migrated from Jetpack:', 'jetpack' ) ) . "\r\n*/\r\n\r\n";
+ $css .= call_user_func( $preprocessors[ $preprocessor ]['callback'], $jetpack_css_post->post_content );
+ } else {
+ $css .= "\r\n\r\n/*\r\n\t" . esc_js( __( 'CSS Migrated from Jetpack:', 'jetpack' ) ) . "\r\n*/\r\n\r\n";
+ $css .= $jetpack_css_post->post_content;
+ }
+
+ wp_update_custom_css_post( $css, array(
+ 'preprocessed' => $pre,
+ ) );
+ }
+
+ Jetpack::log( 'custom_css_4.7_migration', count( $migrated ) . 'revisions migrated' );
+ return count( $migrated );
+ }
+
+ /**
+ * Re-register the legacy CPT so we can play with the content already in the database.
+ */
+ public static function register_legacy_post_type() {
+ if ( post_type_exists( 'safecss' ) ) {
+ return;
+ }
+ // Register safecss as a custom post_type
+ // Explicit capability definitions are largely unnecessary because the posts are manipulated in code via an options page, managing CSS revisions does check the capabilities, so let's ensure that the proper caps are checked.
+ register_post_type( 'safecss', array(
+ 'label' => 'Custom CSS',
+ 'supports' => array( 'revisions' ),
+ 'can_export' => false,
+ 'rewrite' => false,
+ 'capabilities' => array(
+ 'edit_post' => 'edit_theme_options',
+ 'read_post' => 'read',
+ 'delete_post' => 'edit_theme_options',
+ 'edit_posts' => 'edit_theme_options',
+ 'edit_others_posts' => 'edit_theme_options',
+ 'publish_posts' => 'edit_theme_options',
+ 'read_private_posts' => 'read',
+ ),
+ ) );
+ }
+
+ /**
+ * Get the post used for legacy storage.
+ *
+ * Jetpack used to use a single post for all themes, just blanking it on theme switch. This gets that post.
+ *
+ * @return array|bool|null|WP_Post
+ */
+ public static function get_post() {
+ /** This filter is documented in modules/custom-css/custom-css.php */
+ $custom_css_post_id = apply_filters( 'jetpack_custom_css_pre_post_id', null );
+ if ( ! is_null( $custom_css_post_id ) ) {
+ return get_post( $custom_css_post_id );
+ }
+
+ $custom_css_post_id = wp_cache_get( 'custom_css_post_id' );
+
+ if ( false === $custom_css_post_id ) {
+ $custom_css_posts = get_posts( array(
+ 'posts_per_page' => 1,
+ 'post_type' => 'safecss',
+ 'post_status' => 'publish',
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ ) );
+
+ $custom_css_post_id = 0;
+ if ( count( $custom_css_posts ) > 0 ) {
+ $custom_css_post_id = $custom_css_posts[0]->ID;
+ }
+
+ // Save post_id=0 to note that no safecss post exists.
+ wp_cache_set( 'custom_css_post_id', $custom_css_post_id );
+ }
+
+ if ( ! $custom_css_post_id ) {
+ return false;
+ }
+
+ return get_post( $custom_css_post_id );
+ }
+
+ /**
+ * Get all revisions of the Jetpack CSS CPT entry.
+ *
+ * @return array
+ */
+ public static function get_all_revisions() {
+ $post = self::get_post();
+
+ if ( ! $post ) {
+ return array();
+ }
+
+ $revisions = wp_get_post_revisions( $post->ID, array(
+ 'posts_per_page' => -1,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ ) );
+
+ return $revisions;
+ }
+
+ /**
+ * Get the options stored for a given revision ID.
+ *
+ * Jetpack used to version the settings by storing them as meta on the revision.
+ *
+ * @param integer $post_id Post ID.
+ *
+ * @return array
+ */
+ public static function get_options( $post_id = null ) {
+ if ( empty( $post_id ) ) {
+ $post = self::get_post();
+ $post_id = $post->ID;
+ }
+
+ $meta = get_post_meta( $post_id );
+
+ $replace = false;
+ if ( isset( $meta['custom_css_add'][0] ) && 'no' === $meta['custom_css_add'][0] ) {
+ $replace = true;
+ }
+
+ return array(
+ 'preprocessor' => isset( $meta['custom_css_preprocessor'][0] ) ? $meta['custom_css_preprocessor'][0] : '',
+ 'replace' => $replace,
+ 'content_width' => isset( $meta['content_width'][0] ) ? $meta['content_width'][0] : '',
+ );
+ }
+}
+
+Jetpack_Custom_CSS_Data_Migration::add_hooks();
diff --git a/plugins/jetpack/modules/custom-post-types/comics.php b/plugins/jetpack/modules/custom-post-types/comics.php
new file mode 100644
index 00000000..381e9b15
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/comics.php
@@ -0,0 +1,533 @@
+<?php
+
+class Jetpack_Comic {
+ const POST_TYPE = 'jetpack-comic';
+
+ static function init() {
+ static $instance = false;
+
+ if ( ! $instance )
+ $instance = new Jetpack_Comic;
+
+ return $instance;
+ }
+
+ /**
+ * Conditionally hook into WordPress.
+ *
+ * Themes must declare that they support this module by adding
+ * add_theme_support( 'jetpack-comic' ); during after_setup_theme.
+ *
+ * If no theme support is found there is no need to hook into
+ * WordPress. We'll just return early instead.
+ */
+ function __construct() {
+ // Make sure the post types are loaded for imports
+ add_action( 'import_start', array( $this, 'register_post_types' ) );
+
+ // Add to REST API post type whitelist
+ add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_type' ) );
+
+ // If called via REST API, we need to register later in lifecycle
+ add_action( 'restapi_theme_init', array( $this, 'maybe_register_post_types' ) );
+
+ // Return early if theme does not support Jetpack Comic.
+ if ( ! ( $this->site_supports_comics() ) )
+ return;
+
+ $this->register_post_types();
+
+ add_action( 'pre_get_posts', array( $this, 'add_posts_to_loop' ) );
+
+ // In order for the Feedbag job to find Comic posts, we need to circumvent any pretty
+ // URLs in the RSS feed given to Feedbag in favor of /?p=123&post_type=jetpack-comic
+ add_filter( 'the_permalink_rss', array( $this, 'custom_permalink_for_feedbag' ) );
+
+ // There are some cases (like when Feedbag is fetching posts) that the comics
+ // post type needs to be registered no matter what, but none of the UI needs to be
+ // available.
+
+ add_filter( 'post_updated_messages', array( $this, 'updated_messages' ) );
+
+ if ( function_exists( 'queue_publish_post' ) ) {
+ add_action( 'publish_jetpack-comic', 'queue_publish_post', 10, 2 );
+ }
+
+ add_action( 'pre_get_posts', array( $this, 'include_in_feeds' ) );
+
+ add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
+
+ add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( $this, 'manage_posts_columns' ) );
+ add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( $this, 'manage_posts_custom_column' ), 10, 2 );
+ add_image_size( 'jetpack-comic-thumb', 150, 0, false );
+
+ // Enable front-end uploading for users special enough.
+ if ( current_user_can( 'upload_files' ) && current_user_can( 'edit_posts' ) ) {
+ add_action( 'wp_ajax_jetpack_comic_upload', array( $this, 'upload' ) );
+ add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
+ }
+
+ /**
+ * Add a "Convert to Comic" and "Convert to Post" option to the bulk
+ * edit dropdowns.
+ */
+ add_action( 'admin_footer-edit.php', array( $this, 'admin_footer' ) );
+ add_action( 'load-edit.php', array( $this, 'bulk_edit' ) );
+ add_action( 'admin_notices', array( $this, 'bulk_edit_notices' ) );
+
+ }
+
+ public function admin_footer() {
+ $post_type = get_post_type();
+
+ ?>
+ <script type="text/javascript">
+ jQuery( document ).ready( function( $ ) {
+ <?php if ( ! $post_type || 'post' == $post_type ) { ?>
+ $( '<option>' )
+ .val( 'post2comic' )
+ .text( <?php echo json_encode( __( 'Convert to Comic', 'jetpack' ) ); ?> )
+ .appendTo( "select[name='action'], select[name='action2']" );
+ <?php } ?>
+ <?php if ( ! $post_type || self::POST_TYPE == $post_type ) { ?>
+ $( '<option>' )
+ .val( 'comic2post' )
+ .text( <?php echo json_encode( __( 'Convert to Post', 'jetpack' ) ); ?> )
+ .appendTo( "select[name='action'], select[name='action2']" );
+ <?php } ?>
+
+ $( '#message.jetpack-comic-post-type-conversion' ).remove().insertAfter( $( '.wrap h2:first' ) ).show();
+ });
+ </script>
+ <?php
+ }
+
+ /**
+ * Handle the "Convert to [Post|Comic]" bulk action.
+ */
+ public function bulk_edit() {
+ if ( empty( $_REQUEST['post'] ) )
+ return;
+
+ $wp_list_table = _get_list_table( 'WP_Posts_List_Table' );
+ $action = $wp_list_table->current_action();
+
+ check_admin_referer( 'bulk-posts' );
+
+ if ( 'post2comic' == $action || 'comic2post' == $action ) {
+ if ( ! current_user_can( 'publish_posts' ) )
+ wp_die( __( 'You are not allowed to make this change.', 'jetpack' ) );
+
+ $post_ids = array_map( 'intval', $_REQUEST['post'] );
+
+ $modified_count = 0;
+
+ foreach ( $post_ids as $post_id ) {
+ $destination_post_type = ( $action == 'post2comic' ) ? self::POST_TYPE : 'post';
+ $origin_post_type = ( $destination_post_type == 'post' ) ? self::POST_TYPE : 'post';
+
+ if ( current_user_can( 'edit_post', $post_id ) ) {
+ $post = get_post( $post_id );
+
+ // Only convert posts that are post => comic or comic => post.
+ // (e.g., Ignore comic => comic, page => post, etc. )
+ if ( $post->post_type != $destination_post_type && $post->post_type == $origin_post_type ) {
+ $post_type_object = get_post_type_object( $destination_post_type );
+
+ if ( current_user_can( $post_type_object->cap->publish_posts ) ) {
+ set_post_type( $post_id, $destination_post_type );
+ $modified_count++;
+ }
+ }
+ }
+ }
+
+ $sendback = remove_query_arg( array( 'exported', 'untrashed', 'deleted', 'ids' ), wp_get_referer() );
+
+ if ( ! $sendback )
+ $sendback = add_query_arg( array( 'post_type', get_post_type() ), admin_url( 'edit.php' ) );
+
+ $pagenum = $wp_list_table->get_pagenum();
+ $sendback = add_query_arg( array( 'paged' => $pagenum, 'post_type_changed' => $modified_count ), $sendback );
+
+ wp_safe_redirect( $sendback );
+ exit();
+ }
+ }
+
+ /**
+ * Show the post conversion success notice.
+ */
+ public function bulk_edit_notices() {
+ global $pagenow;
+
+ if ( 'edit.php' == $pagenow && ! empty( $_GET['post_type_changed'] ) ) {
+ ?><div id="message" class="updated below-h2 jetpack-comic-post-type-conversion" style="display: none;"><p><?php
+ printf( _n( 'Post converted.', '%s posts converted', $_GET['post_type_changed'], 'jetpack' ), number_format_i18n( $_GET['post_type_changed'] ) );
+ ?></p></div><?php
+ }
+ }
+
+ public function register_scripts() {
+ wp_enqueue_style( 'jetpack-comics-style', plugins_url( 'comics/comics.css', __FILE__ ) );
+ wp_style_add_data( 'jetpack-comics-style', 'rtl', 'replace' );
+
+ wp_enqueue_script(
+ 'jetpack-comics',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/custom-post-types/comics/comics.min.js',
+ 'modules/custom-post-types/comics/comics.js'
+ ),
+ array( 'jquery', 'jquery.spin' )
+ );
+
+ $options = array(
+ 'nonce' => wp_create_nonce( 'jetpack_comic_upload_nonce' ),
+ 'writeURL' => admin_url( 'admin-ajax.php?action=jetpack_comic_upload' ),
+ 'labels' => array(
+ 'dragging' => __( 'Drop images to upload', 'jetpack' ),
+ 'uploading' => __( 'Uploading...', 'jetpack' ),
+ 'processing' => __( 'Processing...', 'jetpack' ),
+ 'unsupported' => __( "Sorry, your browser isn't supported. Upgrade at browsehappy.com.", 'jetpack' ),
+ 'invalidUpload' => __( 'Only images can be uploaded here.', 'jetpack' ),
+ 'error' => __( "Your upload didn't complete; try again later or cross your fingers and try again right now.", 'jetpack' ),
+ )
+ );
+
+ wp_localize_script( 'jetpack-comics', 'Jetpack_Comics_Options', $options );
+ }
+
+ public function admin_enqueue_scripts() {
+ wp_enqueue_style( 'jetpack-comics-admin', plugins_url( 'comics/admin.css', __FILE__ ) );
+ }
+
+ public function maybe_register_post_types() {
+ // Return early if theme does not support Jetpack Comic.
+ if ( ! ( $this->site_supports_comics() ) )
+ return;
+
+ $this->register_post_types();
+ }
+
+ function register_post_types() {
+ if ( post_type_exists( self::POST_TYPE ) ) {
+ return;
+ }
+
+ register_post_type( self::POST_TYPE, array(
+ 'description' => __( 'Comics', 'jetpack' ),
+ 'labels' => array(
+ 'name' => esc_html__( 'Comics', 'jetpack' ),
+ 'singular_name' => esc_html__( 'Comic', 'jetpack' ),
+ 'menu_name' => esc_html__( 'Comics', 'jetpack' ),
+ 'all_items' => esc_html__( 'All Comics', 'jetpack' ),
+ 'add_new' => esc_html__( 'Add New', 'jetpack' ),
+ 'add_new_item' => esc_html__( 'Add New Comic', 'jetpack' ),
+ 'edit_item' => esc_html__( 'Edit Comic', 'jetpack' ),
+ 'new_item' => esc_html__( 'New Comic', 'jetpack' ),
+ 'view_item' => esc_html__( 'View Comic', 'jetpack' ),
+ 'search_items' => esc_html__( 'Search Comics', 'jetpack' ),
+ 'not_found' => esc_html__( 'No Comics found', 'jetpack' ),
+ 'not_found_in_trash' => esc_html__( 'No Comics found in Trash', 'jetpack' ),
+ 'filter_items_list' => esc_html__( 'Filter comics list', 'jetpack' ),
+ 'items_list_navigation' => esc_html__( 'Comics list navigation', 'jetpack' ),
+ 'items_list' => esc_html__( 'Comics list', 'jetpack' ),
+ ),
+ 'supports' => array(
+ 'title',
+ 'editor',
+ 'thumbnail',
+ 'comments',
+ 'revisions',
+ 'publicize', // Jetpack
+ 'subscriptions', // wpcom
+ 'shortlinks', // Jetpack
+ ),
+ 'rewrite' => array(
+ 'slug' => 'comic',
+ 'with_front' => false,
+ ),
+ 'taxonomies' => array(
+ 'category',
+ 'post_tag',
+ ),
+ // Only make the type public for sites that support Comics.
+ 'public' => true,
+ 'menu_position' => 5, // below Posts
+ 'map_meta_cap' => true,
+ 'has_archive' => true,
+ 'query_var' => 'comic',
+ 'show_in_rest' => true,
+ ) );
+ }
+
+ public function manage_posts_columns( $columns ) {
+ $new_columns = array(
+ 'preview-jetpack-comic' => __( 'Preview', 'jetpack' ),
+ );
+ return array_merge( array_slice( $columns, 0, 2 ), $new_columns, array_slice( $columns, 2 ) );
+ }
+
+ public function manage_posts_custom_column( $column_name, $post_ID ) {
+ if ( 'preview-jetpack-comic' == $column_name && has_post_thumbnail( $post_ID ) ) {
+ echo get_the_post_thumbnail( $post_ID, 'jetpack-comic-thumb' );
+ }
+ }
+
+ /**
+ * The function url_to_postid() doesn't handle pretty permalinks
+ * for CPTs very well. When we're generating an RSS feed to be consumed
+ * for Feedbag (the Reader's feed storage mechanism), eschew
+ * a pretty URL for one that will get the post into the Reader.
+ *
+ * @see http://core.trac.wordpress.org/ticket/19744
+ * @param string $permalink The existing (possibly pretty) permalink.
+ */
+ public function custom_permalink_for_feedbag( $permalink ) {
+ global $post;
+
+ if ( ! empty( $GLOBALS['is_feedbag_rss_script'] ) && self::POST_TYPE == $post->post_type ) {
+ $permalink = home_url( add_query_arg( array( 'p' => $post->ID, 'post_type' => self::POST_TYPE ), '?' ) );
+ }
+
+ return $permalink;
+ }
+
+ /*
+ * Update messages for the Comic admin.
+ */
+ function updated_messages( $messages ) {
+ global $post;
+
+ $messages['jetpack-comic'] = array(
+ 0 => '', // Unused. Messages start at index 1.
+ 1 => sprintf( __( 'Comic updated. <a href="%s">View comic</a>', 'jetpack'), esc_url( get_permalink( $post->ID ) ) ),
+ 2 => esc_html__( 'Custom field updated.', 'jetpack' ),
+ 3 => esc_html__( 'Custom field deleted.', 'jetpack' ),
+ 4 => esc_html__( 'Comic updated.', 'jetpack' ),
+ /* translators: %s: date and time of the revision */
+ 5 => isset( $_GET['revision'] ) ? sprintf( esc_html__( 'Comic restored to revision from %s', 'jetpack'), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
+ 6 => sprintf( __( 'Comic published. <a href="%s">View comic</a>', 'jetpack' ), esc_url( get_permalink( $post->ID ) ) ),
+ 7 => esc_html__( 'Comic saved.', 'jetpack' ),
+ 8 => sprintf( __( 'Comic submitted. <a target="_blank" href="%s">Preview comic</a>', 'jetpack'), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ),
+ 9 => sprintf( __( 'Comic scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview comic</a>', 'jetpack' ),
+ // translators: Publish box date format, see http://php.net/date
+ date_i18n( __( 'M j, Y @ G:i', 'jetpack' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post->ID) ) ),
+ 10 => sprintf( __( 'Comic draft updated. <a target="_blank" href="%s">Preview comic</a>', 'jetpack' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ),
+ );
+
+ return $messages;
+ }
+
+ /**
+ * Should this Custom Post Type be made available?
+ */
+ public function site_supports_comics() {
+ /**
+ * @todo: Extract this out into a wpcom only file.
+ */
+ if ( 'blog-rss.php' == substr( $_SERVER['PHP_SELF'], -12 ) && count( $_SERVER['argv'] ) > 1 ) {
+ // blog-rss.php isn't run in the context of the target blog when the init action fires,
+ // so check manually whether the target blog supports comics.
+ switch_to_blog( $_SERVER['argv'][1] );
+ // The add_theme_support( 'jetpack-comic' ) won't fire on switch_to_blog, so check for Panel manually.
+ $supports_comics = ( ( function_exists( 'site_vertical' ) && 'comics' == site_vertical() )
+ || current_theme_supports( self::POST_TYPE )
+ || get_stylesheet() == 'pub/panel' );
+ restore_current_blog();
+
+ /** This action is documented in modules/custom-post-types/nova.php */
+ return (bool) apply_filters( 'jetpack_enable_cpt', $supports_comics, self::POST_TYPE );
+ }
+
+ $supports_comics = false;
+
+ /**
+ * If we're on WordPress.com, and it has the menu site vertical.
+ * @todo: Extract this out into a wpcom only file.
+ */
+ if ( function_exists( 'site_vertical' ) && 'comics' == site_vertical() ) {
+ $supports_comics = true;
+ }
+
+ /**
+ * Else, if the current theme requests it.
+ */
+ if ( current_theme_supports( self::POST_TYPE ) ) {
+ $supports_comics = true;
+ }
+
+ /**
+ * Filter it in case something else knows better.
+ */
+ /** This action is documented in modules/custom-post-types/nova.php */
+ return (bool) apply_filters( 'jetpack_enable_cpt', $supports_comics, self::POST_TYPE );
+ }
+
+ /**
+ * Anywhere that a feed is displaying posts, show comics too.
+ *
+ * @param WP_Query $query
+ */
+ public function include_in_feeds( $query ) {
+ if ( ! $query->is_feed() )
+ return;
+
+ // Don't modify the query if the post type isn't public.
+ if ( ! get_post_type_object( 'jetpack-comic' )->public )
+ return;
+
+ $query_post_types = $query->get( 'post_type' );
+
+ if ( empty( $query_post_types ) )
+ $query_post_types = 'post';
+
+ if ( ! is_array( $query_post_types ) )
+ $query_post_types = array( $query_post_types );
+
+ if ( in_array( 'post', $query_post_types ) ) {
+ $query_post_types[] = self::POST_TYPE;
+ $query->set( 'post_type', $query_post_types );
+ }
+ }
+
+ /**
+ * API endpoint for front-end image uploading.
+ */
+ public function upload() {
+ global $content_width;
+
+ header( 'Content-Type: application/json' );
+
+ if ( ! wp_verify_nonce( $_REQUEST['nonce'], 'jetpack_comic_upload_nonce' ) )
+ die( json_encode( array( 'error' => __( 'Invalid or expired nonce.', 'jetpack' ) ) ) );
+
+ $_POST['action'] = 'wp_handle_upload';
+
+ $image_id_arr = array();
+ $image_error_arr = array();
+
+ $i = 0;
+
+ while ( isset( $_FILES['image_' . $i ] ) ) {
+ // Create attachment for the image.
+ $image_id = media_handle_upload( "image_$i", 0 );
+
+ if ( is_wp_error( $image_id ) ) {
+ $error = array( $image_id, $image_id->get_error_message() );
+ array_push( $image_error_arr, $error );
+ } else {
+ array_push( $image_id_arr, $image_id );
+ }
+
+ $i++;
+ }
+
+ if ( count( $image_id_arr ) == 0 ) {
+ // All image uploads failed.
+ $rv = array( 'error' => '' );
+
+ foreach ( $image_error_arr as $error )
+ $rv['error'] .= $error[1] . "\n";
+ }
+ else {
+ if ( count( $image_id_arr ) == 1 ) {
+ $image_id = $image_id_arr[0];
+
+ // Get the image
+ $image_src = get_the_guid( $image_id );
+ $image_dims = wp_get_attachment_image_src( $image_id, 'full' );
+
+ // Take off 10px of width to account for padding and border. @todo make this smarter.
+ if ( $content_width )
+ $image_width = $content_width - 10;
+ else
+ $image_width = $image_dims[1] - 10;
+
+ $post_content = '<a href="' . esc_attr( $image_src ) .'"><img src="' . esc_attr( $image_src ) . '?w=' . esc_attr( $image_width ) . '" alt="' . esc_attr( $_FILES['image_0']['name'] ) . '" class="size-full wp-image alignnone" id="i-' . esc_attr( $image_id ) . '" data-filename="' . esc_attr( $_FILES['image_0']['name'] ) . '" /></a>';
+ }
+ else {
+ $post_content = '[gallery ids="' . esc_attr( implode( ',', $image_id_arr ) ) . '"]';
+ }
+
+ // Create a new post with the image(s)
+ $post_id = wp_insert_post( array(
+ 'post_content' => $post_content,
+ 'post_type' => 'jetpack-comic',
+ 'post_status' => 'draft',
+ ),
+ true
+ );
+
+ if ( is_wp_error( $post_id, 'WP_Error' ) ) {
+ // Failed to create the post.
+ $rv = array( 'error' => $post_id->get_error_message() );
+
+ // Delete the uploaded images.
+ foreach ( $image_id_arr as $image_id ) {
+ wp_delete_post( $image_id, true );
+ }
+ }
+ else {
+ foreach ( $image_id_arr as $image_id ) {
+ wp_update_post( array(
+ 'ID' => $image_id,
+ 'post_parent' => $post_id
+ ) );
+ }
+
+ if ( current_theme_supports( 'post-thumbnails' ) && count( $image_id_arr ) == 1 )
+ set_post_thumbnail( $post_id, $image_id_arr[0] );
+
+ $rv = array( 'url' => add_query_arg( array( 'post' => $post_id, 'action' => 'edit' ), admin_url( 'post.php' ) ) );
+ }
+ }
+
+ die( json_encode( $rv ) );
+ }
+
+ public function add_posts_to_loop( $query ) {
+ // Add comic posts to the tag and category pages.
+ if ( ! is_admin() && $query->is_main_query() && ( $query->is_category() || $query->is_tag() ) ) {
+ $post_types = $query->get( 'post_type' );
+
+ if ( ! $post_types || 'post' == $post_types )
+ $post_types = array( 'post', self::POST_TYPE );
+ else if ( is_array( $post_types ) )
+ $post_types[] = self::POST_TYPE;
+
+ $query->set( 'post_type', $post_types );
+ }
+
+ return $query;
+ }
+
+ /**
+ * Add to REST API post type whitelist
+ */
+ public function allow_rest_api_type( $post_types ) {
+ $post_types[] = self::POST_TYPE;
+ return $post_types;
+ }
+
+}
+
+add_action( 'init', array( 'Jetpack_Comic', 'init' ) );
+
+
+function comics_welcome_email( $welcome_email, $blog_id, $user_id, $password, $title, $meta ) {
+ if ( ( isset( $meta['vertical'] ) && 'comics' == $meta['vertical'] ) || has_blog_sticker( 'vertical-comics', $blog_id ) ) {
+ return __( "Welcome! Ready to publish your first strip?
+
+Your webcomic's new site is ready to go. Get started by <a href=\"BLOG_URLwp-admin/customize.php#title\">setting your comic's title and tagline</a> so your readers know what it's all about.
+
+Looking for more help with setting up your site? Check out the WordPress.com <a href=\"http://learn.wordpress.com/\" target=\"_blank\">beginner's tutorial</a> and the <a href=\"http://en.support.wordpress.com/comics/\" target=\"_blank\">guide to comics on WordPress.com</a>. Dive right in by <a href=\"BLOG_URLwp-admin/customize.php#title\">publishing your first strip!</a>
+
+Lots of laughs,
+The WordPress.com Team", 'jetpack' );
+ }
+
+ return $welcome_email;
+}
+
+add_filter( 'update_welcome_email_pre_replacement', 'comics_welcome_email', 10, 6 );
diff --git a/plugins/jetpack/modules/custom-post-types/comics/admin.css b/plugins/jetpack/modules/custom-post-types/comics/admin.css
new file mode 100644
index 00000000..e347ac3c
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/comics/admin.css
@@ -0,0 +1,7 @@
+#adminmenu #menu-posts-jetpack-comic .menu-icon-post div.wp-menu-image:before {
+ content: '\f125';
+}
+
+.edit-php .column-preview-jetpack-comic {
+ width: 150px;
+}
diff --git a/plugins/jetpack/modules/custom-post-types/comics/comics-rtl.css b/plugins/jetpack/modules/custom-post-types/comics/comics-rtl.css
new file mode 100644
index 00000000..7662d7e5
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/comics/comics-rtl.css
@@ -0,0 +1,31 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#jetpack-comic-drop-zone .dragging, #jetpack-comic-drop-zone .uploading {
+ margin-top: 200px;
+ display: none;
+}
+
+/* Display the appropriate loading message for each upload state. */
+body.dragging #jetpack-comic-drop-zone .dragging, body.uploading #jetpack-comic-drop-zone .uploading {
+ display: block;
+}
+
+body.uploading #jetpack-comic-drop-zone .uploading .spinner {
+ display: inline-block;
+ width: 60px;
+}
+
+/* Add the drop zone overlay. */
+body.dragging #jetpack-comic-drop-zone, body.uploading #jetpack-comic-drop-zone {
+ background: rgba( 0, 86, 132, 0.9 );
+ border: 1px dashed #fff;
+ color: #fff;
+ display: block;
+ font-size: 30px;
+ position: fixed;
+ top: 10px;
+ right: 10px;
+ left: 10px;
+ bottom: 10px;
+ text-align: center;
+ z-index: 99999;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-post-types/comics/comics-rtl.min.css b/plugins/jetpack/modules/custom-post-types/comics/comics-rtl.min.css
new file mode 100644
index 00000000..9fa3feae
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/comics/comics-rtl.min.css
@@ -0,0 +1 @@
+#jetpack-comic-drop-zone .dragging,#jetpack-comic-drop-zone .uploading{margin-top:200px;display:none}body.dragging #jetpack-comic-drop-zone .dragging,body.uploading #jetpack-comic-drop-zone .uploading{display:block}body.uploading #jetpack-comic-drop-zone .uploading .spinner{display:inline-block;width:60px}body.dragging #jetpack-comic-drop-zone,body.uploading #jetpack-comic-drop-zone{background:rgba(0,86,132,.9);border:1px dashed #fff;color:#fff;display:block;font-size:30px;position:fixed;top:10px;right:10px;left:10px;bottom:10px;text-align:center;z-index:99999} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-post-types/comics/comics.css b/plugins/jetpack/modules/custom-post-types/comics/comics.css
new file mode 100644
index 00000000..6e5cf110
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/comics/comics.css
@@ -0,0 +1,30 @@
+#jetpack-comic-drop-zone .dragging, #jetpack-comic-drop-zone .uploading {
+ margin-top: 200px;
+ display: none;
+}
+
+/* Display the appropriate loading message for each upload state. */
+body.dragging #jetpack-comic-drop-zone .dragging, body.uploading #jetpack-comic-drop-zone .uploading {
+ display: block;
+}
+
+body.uploading #jetpack-comic-drop-zone .uploading .spinner {
+ display: inline-block;
+ width: 60px;
+}
+
+/* Add the drop zone overlay. */
+body.dragging #jetpack-comic-drop-zone, body.uploading #jetpack-comic-drop-zone {
+ background: rgba( 0, 86, 132, 0.9 );
+ border: 1px dashed #fff;
+ color: #fff;
+ display: block;
+ font-size: 30px;
+ position: fixed;
+ top: 10px;
+ left: 10px;
+ right: 10px;
+ bottom: 10px;
+ text-align: center;
+ z-index: 99999;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-post-types/comics/comics.js b/plugins/jetpack/modules/custom-post-types/comics/comics.js
new file mode 100644
index 00000000..159cf3bd
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/comics/comics.js
@@ -0,0 +1,134 @@
+/* jshint onevar: false, smarttabs: true, devel: true */
+/* global Jetpack_Comics_Options */
+
+jQuery( function( $ ) {
+ /**
+ * Enable front-end uploading of images for Comics users.
+ */
+ var Jetpack_Comics = {
+ init: function() {
+ $( document ).on(
+ 'dragover.jetpack-comics',
+ 'body, #jetpack-comic-drop-zone',
+ this.onDragOver
+ );
+ $( document ).on(
+ 'dragleave.jetpack-comics',
+ 'body, #jetpack-comic-drop-zone',
+ this.onDragLeave
+ );
+ $( document ).on( 'drop.jetpack-comics', 'body, #jetpack-comic-drop-zone', this.onDrop );
+
+ $( 'body' ).append(
+ $( '<div id="jetpack-comic-drop-zone"><p class="dragging" /><p class="uploading" /></div>' )
+ );
+ $( '#jetpack-comic-drop-zone' )
+ .find( '.dragging' )
+ .text( Jetpack_Comics_Options.labels.dragging )
+ .end()
+ .find( '.uploading' )
+ .text( Jetpack_Comics_Options.labels.uploading )
+ .prepend( $( '<span class="spinner"/>' ) );
+
+ if ( ! ( 'FileReader' in window && 'File' in window ) ) {
+ $( '#jetpack-comic-drop-zone .dragging' ).text( Jetpack_Comics_Options.labels.unsupported );
+ $( document )
+ .off( 'drop.jetpack-comics' )
+ .on( 'drop.jetpack-comics', 'body, #jetpack-comic-drop-zone', this.onDragLeave );
+ }
+ },
+
+ /**
+ * Only upload image files.
+ */
+ filterImageFiles: function( files ) {
+ var validFiles = [];
+
+ for ( var i = 0, _len = files.length; i < _len; i++ ) {
+ if ( files[ i ].type.match( /^image\//i ) ) {
+ validFiles.push( files[ i ] );
+ }
+ }
+
+ return validFiles;
+ },
+
+ dragTimeout: null,
+
+ onDragOver: function( event ) {
+ event.preventDefault();
+
+ clearTimeout( Jetpack_Comics.dragTimeout );
+
+ $( 'body' ).addClass( 'dragging' );
+ },
+
+ onDragLeave: function(/*event*/) {
+ clearTimeout( Jetpack_Comics.dragTimeout );
+
+ // In Chrome, the screen flickers because we're moving the drop zone in front of 'body'
+ // so the dragover/dragleave events happen frequently.
+ Jetpack_Comics.dragTimeout = setTimeout( function() {
+ $( 'body' ).removeClass( 'dragging' );
+ }, 100 );
+ },
+
+ onDrop: function( event ) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ // recent chrome bug requires this, see stackoverflow thread: http://bit.ly/13BU7b5
+ event.originalEvent.stopPropagation();
+ event.originalEvent.preventDefault();
+
+ var files = Jetpack_Comics.filterImageFiles( event.originalEvent.dataTransfer.files );
+
+ $( 'body' ).removeClass( 'dragging' );
+
+ if ( files.length === 0 ) {
+ alert( Jetpack_Comics_Options.labels.invalidUpload );
+ return;
+ }
+
+ $( 'body' ).addClass( 'uploading' );
+
+ var formData = new FormData();
+
+ for ( var i = 0, fl = files.length; i < fl; i++ ) {
+ formData.append( 'image_' + i, files[ i ] ); // won't work as image[]
+ }
+
+ $( '#jetpack-comic-drop-zone .uploading .spinner' ).spin();
+
+ $.ajax( {
+ url: Jetpack_Comics_Options.writeURL + '&nonce=' + Jetpack_Comics_Options.nonce,
+ data: formData,
+ processData: false,
+ contentType: false,
+ type: 'POST',
+ dataType: 'json',
+ xhrFields: {
+ withCredentials: true,
+ },
+ } )
+ .done( function( data ) {
+ $( '#jetpack-comic-drop-zone .uploading' ).text(
+ Jetpack_Comics_Options.labels.processing
+ );
+
+ if ( 'url' in data ) {
+ document.location.href = data.url;
+ } else if ( 'error' in data ) {
+ alert( data.error );
+
+ $( 'body' ).removeClass( 'uploading' );
+ }
+ } )
+ .fail( function(/*req*/) {
+ alert( Jetpack_Comics_Options.labels.error );
+ } );
+ },
+ };
+
+ Jetpack_Comics.init();
+} );
diff --git a/plugins/jetpack/modules/custom-post-types/comics/comics.min.css b/plugins/jetpack/modules/custom-post-types/comics/comics.min.css
new file mode 100644
index 00000000..2526a81c
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/comics/comics.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#jetpack-comic-drop-zone .dragging,#jetpack-comic-drop-zone .uploading{margin-top:200px;display:none}body.dragging #jetpack-comic-drop-zone .dragging,body.uploading #jetpack-comic-drop-zone .uploading{display:block}body.uploading #jetpack-comic-drop-zone .uploading .spinner{display:inline-block;width:60px}body.dragging #jetpack-comic-drop-zone,body.uploading #jetpack-comic-drop-zone{background:rgba(0,86,132,.9);border:1px dashed #fff;color:#fff;display:block;font-size:30px;position:fixed;top:10px;left:10px;right:10px;bottom:10px;text-align:center;z-index:99999} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-post-types/comics/rtl/comics-rtl.css b/plugins/jetpack/modules/custom-post-types/comics/rtl/comics-rtl.css
new file mode 100644
index 00000000..773fd99d
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/comics/rtl/comics-rtl.css
@@ -0,0 +1,32 @@
+/* This file was automatically generated on Jul 09 2013 05:18:56 */
+
+#jetpack-comic-drop-zone .dragging, #jetpack-comic-drop-zone .uploading {
+ margin-top: 200px;
+ display: none;
+}
+
+/* Display the appropriate loading message for each upload state. */
+body.dragging #jetpack-comic-drop-zone .dragging, body.uploading #jetpack-comic-drop-zone .uploading {
+ display: block;
+}
+
+body.uploading #jetpack-comic-drop-zone .uploading .spinner {
+ display: inline-block;
+ width: 60px;
+}
+
+/* Add the drop zone overlay. */
+body.dragging #jetpack-comic-drop-zone, body.uploading #jetpack-comic-drop-zone {
+ background: rgba( 0, 86, 132, 0.9 );
+ border: 1px dashed #fff;
+ color: #fff;
+ display: block;
+ font-size: 30px;
+ position: fixed;
+ top: 10px;
+ right: 10px;
+ left: 10px;
+ bottom: 10px;
+ text-align: center;
+ z-index: 99999;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-post-types/css/edit-items.css b/plugins/jetpack/modules/custom-post-types/css/edit-items.css
new file mode 100644
index 00000000..85fbbe96
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/css/edit-items.css
@@ -0,0 +1,24 @@
+.widefat .menu-label-row td {
+ border-bottom-width: 1px;
+}
+.widefat .menu-label-row td h3 {
+ padding-left: 30px;
+}
+.widefat .menu-order-value {
+ width: 2.5em;
+ text-align: center;
+}
+.widefat .menu-label-row, .widefat .menu-label-row td {
+ background-color: #d6d6d6;
+ color: #111;
+ border: 0 none;
+}
+.ui-sortable .type-nova_menu_item {
+ cursor: move;
+}
+.tablenav .button-reorder {
+ margin-top: 4px;
+}
+.tablenav .view-switch a, .tablenav div.tablenav-pages {
+ display: none;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-post-types/css/many-items.css b/plugins/jetpack/modules/custom-post-types/css/many-items.css
new file mode 100644
index 00000000..c9932430
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/css/many-items.css
@@ -0,0 +1,14 @@
+.many-items-table th, .many-items-table td {
+ width: 25%;
+}
+
+.many-items-table input, .many-items-table textarea {
+ width: 100%;
+}
+
+.many-items-table input[type=file] {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+}
diff --git a/plugins/jetpack/modules/custom-post-types/css/nova-font.css b/plugins/jetpack/modules/custom-post-types/css/nova-font.css
new file mode 100644
index 00000000..ac1b9067
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/css/nova-font.css
@@ -0,0 +1,30 @@
+@font-face {
+ font-family: 'nova-font';
+ src: url('../fonts/nova.eot');
+}
+@font-face {
+ font-family: 'nova-font';
+ src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg5lAuAAAAC8AAAAYGNtYXDL9xqaAAABHAAAADxnYXNwAAAAEAAAAVgAAAAIZ2x5Zrlfj0YAAAFgAAABrGhlYWQAW+atAAADDAAAADZoaGVhB2ED4AAAA0QAAAAkaG10eAXcAGQAAANoAAAADGxvY2EACgDWAAADdAAAAAhtYXhwAAgAkQAAA3wAAAAgbmFtZXvEneAAAAOcAAABHnBvc3QAAwAAAAAEvAAAACAAAwPoAZAABQAAAooCvAAAAIwCigK8AAAB4AAxAQIAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAACDmAwOp/8L/wgOpAD4AAAAAAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEACgAAAAGAAQAAQACACDmA///AAAAIOYD////4Rn/AAEAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAQAZAAyA7YDhAAoAEUAfQCOAAABMhY6ATMyPgI/ASMHJz8BDwEnNycHDgMHFgYWBhcHMgYWIjMXNwUHDgMHHgMXHgMzMj4CPwEnKgMjFyc3Ni4CJy4DIyIOAgcOAxceAxceAz8BAR4DMzI+Ajc+Ayc2LgIvAQEmPgI3PgEeARceAxcnApQCBAQEAg4XFxQKtzGXH2oaM3UhlwHUCg0KBAEBAQIBAhQBAQEBAXIN/t3oCwwLAwEBAwsMCwgVFhkMDhgXEwuxcgICAwEC94wFBQMUIRkRLS0xFQsVFxULCQ8GAgQDDxMaDxUuNDIaDgEwBhAQFQkMExIQCAcMBgUBAQUGDAfm/i8CAwIHAhApLy0VCRMMCwLrAfUBBAoNCtWWIHYyGWsglDS5CRQWGQ0CBAQEAw8BAWkOKsIJFBYZDQ0YFxQJCg0KBAQKDQrWngp+DxczNTUXEx4VDAMHDAoKGh4iExMnJSMPFB4SBQQD/l8HCgYEBAcLCAcQEhQKCxMSEAjPAV8BBgcHBA4KCRgTCxgWFQlaAAAAAAEAAAABAAAD2anvXw889QALA+gAAAAAzsPRIgAAAADOw9EiAAAAAAO2A4QAAAAIAAIAAAAAAAAAAQAAA6n/wgAAA+gAAAAyA7YAAQAAAAAAAAAAAAAAAAAAAAMAAAAAAfQAAAPoAGQAAAAAAAoA1gABAAAAAwCPAAQAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEACAAAAAEAAAAAAAIADgAyAAEAAAAAAAMACAAeAAEAAAAAAAQACABAAAEAAAAAAAUAFgAIAAEAAAAAAAYABAAmAAEAAAAAAAoAKABIAAMAAQQJAAEACAAAAAMAAQQJAAIADgAyAAMAAQQJAAMACAAeAAMAAQQJAAQACABAAAMAAQQJAAUAFgAIAAMAAQQJAAYACAAqAAMAAQQJAAoAKABIAG4AbwB2AGEAVgBlAHIAcwBpAG8AbgAgADAALgAwAG4AbwB2AGFub3ZhAG4AbwB2AGEAUgBlAGcAdQBsAGEAcgBuAG8AdgBhAEcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('truetype'),
+ url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAUoAAsAAAAABNwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABCAAAAGAAAABgDmUC4GNtYXAAAAFoAAAAPAAAADzL9xqaZ2FzcAAAAaQAAAAIAAAACAAAABBnbHlmAAABrAAAAawAAAGsuV+PRmhlYWQAAANYAAAANgAAADYAW+ataGhlYQAAA5AAAAAkAAAAJAdhA+BobXR4AAADtAAAAAwAAAAMBdwAZGxvY2EAAAPAAAAACAAAAAgACgDWbWF4cAAAA8gAAAAgAAAAIAAIAJFuYW1lAAAD6AAAAR4AAAEee8Sd4HBvc3QAAAUIAAAAIAAAACAAAwAAAAMD6AGQAAUAAAKKArwAAACMAooCvAAAAeAAMQECAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg5gMDqf/C/8IDqQA+AAAAAAAAAAAAAAAAAAAAIAAAAAAAAgAAAAMAAAAUAAMAAQAAABQABAAoAAAABgAEAAEAAgAg5gP//wAAACDmA////+EZ/wABAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAAEAGQAMgO2A4QAKABFAH0AjgAAATIWOgEzMj4CPwEjByc/AQ8BJzcnBw4DBxYGFgYXBzIGFiIzFzcFBw4DBx4DFx4DMzI+Aj8BJyoDIxcnNzYuAicuAyMiDgIHDgMXHgMXHgM/AQEeAzMyPgI3PgMnNi4CLwEBJj4CNz4BHgEXHgMXJwKUAgQEBAIOFxcUCrcxlx9qGjN1IZcB1AoNCgQBAQECAQIUAQEBAQFyDf7d6AsMCwMBAQMLDAsIFRYZDA4YFxMLsXICAgMBAveMBQUDFCEZES0tMRULFRcVCwkPBgIEAw8TGg8VLjQyGg4BMAYQEBUJDBMSEAgHDAYFAQEFBgwH5v4vAgMCBwIQKS8tFQkTDAsC6wH1AQQKDQrVliB2MhlrIJQ0uQkUFhkNAgQEBAMPAQFpDirCCRQWGQ0NGBcUCQoNCgQECg0K1p4Kfg8XMzU1FxMeFQwDBwwKChoeIhMTJyUjDxQeEgUEA/5fBwoGBAQHCwgHEBIUCgsTEhAIzwFfAQYHBwQOCgkYEwsYFhUJWgAAAAABAAAAAQAAA9mp718PPPUACwPoAAAAAM7D0SIAAAAAzsPRIgAAAAADtgOEAAAACAACAAAAAAAAAAEAAAOp/8IAAAPoAAAAMgO2AAEAAAAAAAAAAAAAAAAAAAADAAAAAAH0AAAD6ABkAAAAAAAKANYAAQAAAAMAjwAEAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAgAAAABAAAAAAACAA4AMgABAAAAAAADAAgAHgABAAAAAAAEAAgAQAABAAAAAAAFABYACAABAAAAAAAGAAQAJgABAAAAAAAKACgASAADAAEECQABAAgAAAADAAEECQACAA4AMgADAAEECQADAAgAHgADAAEECQAEAAgAQAADAAEECQAFABYACAADAAEECQAGAAgAKgADAAEECQAKACgASABuAG8AdgBhAFYAZQByAHMAaQBvAG4AIAAwAC4AMABuAG8AdgBhbm92YQBuAG8AdgBhAFIAZQBnAHUAbABhAHIAbgBvAHYAYQBHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
+
+#menu-posts-nova_menu_item:before,
+#dashboard_right_now .nova-menu-count a:before,
+#dashboard_right_now .nova-menu-count span:before {
+ font-family: 'nova-font';
+ speak: none;
+ font-style: normal;
+ font-weight: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+
+ /* Better Font Rendering =========== */
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+#dashboard_right_now .nova-menu-count a:before, #dashboard_right_now .nova-menu-count span:before {
+ content: '\e603';
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-post-types/css/nova.css b/plugins/jetpack/modules/custom-post-types/css/nova.css
new file mode 100644
index 00000000..309b510f
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/css/nova.css
@@ -0,0 +1,110 @@
+/* edit-items.css
+-------------------------------------------------------------- */
+
+.widefat .menu-label-row td {
+ border-bottom-width: 1px;
+}
+.widefat .menu-label-row td h3 {
+ padding-left: 30px;
+}
+.widefat .menu-label-row td h3 .edit-nova-section {
+ font-size: .8em;
+ font-weight: normal;
+ margin-left: 5px;
+}
+.widefat .menu-order-value {
+ width: 2.5em;
+ text-align: center;
+}
+.widefat .menu-label-row, .widefat .menu-label-row td {
+ background-color: #eee;
+ color: #111;
+ border: 0 none;
+ -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.05);
+}
+.ui-sortable .type-nova_menu_item {
+ cursor: move;
+ background-color: #fff;
+}
+.ui-sortable .type-nova_menu_item:nth-child(even) {
+ background-color: #f9f9f9;
+}
+.ui-sortable .type-nova_menu_item.ui-sortable-helper {
+ -webkit-box-shadow: 0 0 1px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 0 1px rgba(0, 0, 0, 0.1);
+}
+.tablenav .button-reorder {
+ margin-top: 4px;
+}
+.tablenav .view-switch a, .tablenav div.tablenav-pages {
+ display: none;
+}
+
+
+/* many-items.css
+-------------------------------------------------------------- */
+
+.many-items-table th, .many-items-table td {
+ width: 30%;
+}
+
+.many-items-table th.nova-price, .many-items-table td.nova-price {
+ width: 10%;
+}
+
+.many-items-table input, .many-items-table textarea {
+ width: 100%;
+}
+
+.many-items-table input[type=file] {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+
+/* new
+-------------------------------------------------------------- */
+
+#the-list tr td:nth-of-type(2) {
+ padding-top: 15px;
+}
+
+.nova-move-menu-up:before,
+.nova-move-menu-down:before {
+ margin-right: 5px;
+ font: normal 10px/1 'dashicons' !important;
+ speak: none;
+}
+
+.nova-move-menu-up:before {
+ content: "\f342";
+}
+
+.nova-move-menu-down:before {
+ content: "\f346";
+}
+
+.dashicon:before {
+ font: normal 20px/1 'dashicons';
+ speak: none;
+ top: 5px;
+ display: inline-block;
+ position: relative;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-decoration: none !important;
+ vertical-align: top;
+}
+
+.dashicon-plus:before {
+ content: "\f132";
+}
+
+.dashicon-edit:before {
+ margin: 2px 5px 0 10px;
+ content: "\f327";
+ font-size: 10px;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-post-types/css/portfolio-shortcode.css b/plugins/jetpack/modules/custom-post-types/css/portfolio-shortcode.css
new file mode 100644
index 00000000..b3c2c1e3
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/css/portfolio-shortcode.css
@@ -0,0 +1,131 @@
+.jetpack-portfolio-shortcode {
+ clear: both;
+ margin: 0;
+ overflow: hidden;
+ padding: 0;
+}
+
+.portfolio-entry {
+ float: left;
+ margin: 0 0 3em;
+ padding: 0;
+ width: 100%;
+}
+
+/* Column setting */
+.portfolio-entry-column-1 {
+ width: 100%;
+}
+
+.portfolio-entry-column-2 {
+ margin-right: 4%;
+ width: 48%;
+}
+
+.portfolio-entry-column-3 {
+ margin-right: 3.5%;
+ width: 31%;
+}
+
+.portfolio-entry-column-4 {
+ margin-right: 3%;
+ width: 22%;
+}
+
+.portfolio-entry-column-5 {
+ margin-right: 2.5%;
+ width: 18%;
+}
+
+.portfolio-entry-column-6 {
+ margin-right: 2%;
+ width: 15%;
+}
+.portfolio-entry-first-item-row {
+ clear: both;
+}
+.portfolio-entry-last-item-row {
+ margin-right: 0;
+}
+
+@media screen and (max-width:768px) {
+ .portfolio-entry-mobile-first-item-row{
+ margin-right: 4%;
+ width: 48%;
+ clear:both;
+ }
+ .portfolio-entry-first-item-row {
+ clear:none;
+ }
+ .portfolio-entry-mobile-last-item-row{
+ width: 48%;
+ margin-right: 0;
+ }
+}
+/* Entry Header */
+.portfolio-entry-header {
+ border: 0;
+ margin: 0;
+ padding: 0;
+}
+
+.portfolio-featured-image {
+ margin: 0;
+ padding: 0;
+}
+
+.portfolio-featured-image img {
+ border: 0;
+ height: auto;
+ max-width: 100%;
+ vertical-align: middle;
+}
+
+.portfolio-entry-title {
+ font-weight: 700;
+ margin: 0;
+ padding: 0;
+}
+
+.portfolio-featured-image + .portfolio-entry-title {
+ margin-top: 1.0em;
+}
+
+.portfolio-entry-title a {
+ border: 0;
+ text-decoration: none;
+}
+
+/* Entry Meta */
+.portfolio-entry-meta {
+ margin: 0;
+ padding: 0;
+}
+
+.portfolio-entry-title + .portfolio-entry-meta {
+ margin-top: 0.75em;
+}
+
+.portfolio-entry-title + .portfolio-entry-meta:empty {
+ margin: 0;
+}
+
+.portfolio-entry-meta span,
+.portfolio-entry-meta a {
+ font-size: 0.9em;
+ padding: 0;
+}
+
+.portfolio-entry-meta a {
+ border: 0;
+ text-decoration: none;
+}
+/* Entry Content */
+.portfolio-entry-content {
+ margin: 0.75em 0 0;
+ padding: 0;
+}
+
+.portfolio-entry-content > :last-child {
+ margin: 0;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-post-types/css/testimonial-shortcode.css b/plugins/jetpack/modules/custom-post-types/css/testimonial-shortcode.css
new file mode 100644
index 00000000..57a35bc8
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/css/testimonial-shortcode.css
@@ -0,0 +1,102 @@
+.jetpack-testimonial-shortcode {
+ clear: both;
+ margin: 0;
+ overflow: hidden;
+ padding: 0;
+}
+
+.testimonial-entry {
+ float: left;
+ margin: 0 0 3em;
+ padding: 0;
+ width: 100%;
+}
+
+/* Column setting */
+.testimonial-entry-column-1 {
+ width: 100%;
+}
+
+.testimonial-entry-column-2 {
+ margin-right: 4%;
+ width: 48%;
+}
+
+.testimonial-entry-column-3 {
+ margin-right: 3.5%;
+ width: 31%;
+}
+
+.testimonial-entry-column-4 {
+ margin-right: 3%;
+ width: 22%;
+}
+
+.testimonial-entry-column-5 {
+ margin-right: 2.5%;
+ width: 18%;
+}
+
+.testimonial-entry-column-6 {
+ margin-right: 2%;
+ width: 15%;
+}
+.testimonial-entry-first-item-row {
+ clear: both;
+}
+.testimonial-entry-last-item-row {
+ margin-right: 0;
+}
+
+@media screen and (max-width:768px) {
+ .testimonial-entry-mobile-first-item-row{
+ margin-right: 4%;
+ width: 48%;
+ clear:both;
+ }
+ .testimonial-entry-first-item-row {
+ clear:none;
+ }
+ .testimonial-entry-mobile-last-item-row{
+ width: 48%;
+ margin-right: 0;
+ }
+}
+
+.testimonial-featured-image {
+ padding: 0;
+ margin: 0;
+}
+
+.testimonial-featured-image img {
+ border: 0;
+ height: auto;
+ max-width: 100%;
+ vertical-align: middle;
+}
+
+.testimonial-entry-title {
+ font-weight: 700;
+ margin: 0;
+ padding: 0;
+ display: block;
+}
+
+.testimonial-featured-image + .testimonial-entry-title {
+ margin-top: 1.0em;
+}
+
+.testimonial-entry-title a {
+ border: 0;
+ text-decoration: none;
+}
+
+/* Entry Content */
+.testimonial-entry-content {
+ margin: 0.75em 0;
+ padding: 0;
+}
+
+.testimonial-entry-content > :last-child {
+ margin: 0;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/custom-post-types/js/many-items.js b/plugins/jetpack/modules/custom-post-types/js/many-items.js
new file mode 100644
index 00000000..3e67e143
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/js/many-items.js
@@ -0,0 +1,116 @@
+/* jshint onevar: false, smarttabs: true */
+
+( function( $ ) {
+ var menuSelector, nonceInput, methods;
+
+ methods = {
+ init: function(/*options*/) {
+ var $this = this,
+ tbody,
+ row;
+
+ this.on( 'keypress.manyItemsTable', function( event ) {
+ if ( 13 !== event.which ) {
+ return;
+ }
+
+ event.preventDefault();
+ if ( 'function' === typeof FormData ) {
+ methods.submitRow.apply( $this );
+ }
+ methods.addRow.apply( $this );
+ } ).on( 'focus.manyItemsTable', ':input', function(/*event*/) {
+ $this.data( 'currentRow', $( this ).parents( 'tr:first' ) );
+ } );
+
+ tbody = this.find( 'tbody:last' );
+ row = tbody.find( 'tr:first' ).clone();
+
+ this.data( 'form', this.parents( 'form:first' ) );
+ this.data( 'tbody', tbody );
+ this.data( 'row', row );
+ this.data( 'currentRow', row );
+
+ menuSelector = $( '#nova-menu-tax' );
+ nonceInput = $( '#_wpnonce' );
+
+ return this;
+ },
+
+ destroy: function() {
+ this.off( '.manyItemsTable' );
+
+ return this;
+ },
+
+ submitRow: function() {
+ var submittedRow, currentInputs, allInputs, partialFormData;
+
+ submittedRow = this.data( 'currentRow' );
+ currentInputs = submittedRow.find( ':input' );
+ allInputs = this.data( 'form' )
+ .find( ':input' )
+ .not( currentInputs )
+ .attr( 'disabled', true )
+ .end();
+
+ partialFormData = new FormData( this.data( 'form' ).get( 0 ) );
+ partialFormData.append( 'ajax', '1' );
+ partialFormData.append( 'nova_menu_tax', menuSelector.val() );
+ partialFormData.append( '_wpnonce', nonceInput.val() );
+
+ allInputs.attr( 'disabled', false );
+
+ $.ajax( {
+ url: '',
+ type: 'POST',
+ data: partialFormData,
+ processData: false,
+ contentType: false,
+ } ).complete( function( xhr ) {
+ submittedRow.html( xhr.responseText );
+ } );
+
+ currentInputs.attr( 'disabled', true );
+
+ return this;
+ },
+
+ addRow: function() {
+ var row = this.data( 'row' ).clone();
+ row.appendTo( this.data( 'tbody' ) );
+ row.find( ':input:first' ).focus();
+
+ return this;
+ },
+ };
+
+ $.fn.manyItemsTable = function( method ) {
+ // Method calling logic
+ if ( methods[ method ] ) {
+ return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) );
+ } else if ( typeof method === 'object' || ! method ) {
+ return methods.init.apply( this, arguments );
+ } else {
+ $.error( 'Method ' + method + ' does not exist on jQuery.manyItemsTable' );
+ return this;
+ }
+ };
+
+ $.fn.clickAddRow = function() {
+ var tbody = this.find( 'tbody:last' ),
+ row = tbody.find( 'tr:first' ).clone();
+
+ $( row )
+ .find( 'input, textarea' )
+ .val( '' );
+ $( row ).appendTo( tbody );
+ };
+} )( jQuery );
+
+jQuery( '.many-items-table' ).one( 'focus', ':input', function( event ) {
+ jQuery( event.delegateTarget ).manyItemsTable();
+} );
+jQuery( '.many-items-table' ).on( 'click', 'a.nova-new-row', function( event ) {
+ jQuery( event.delegateTarget ).clickAddRow();
+} );
diff --git a/plugins/jetpack/modules/custom-post-types/js/menu-checkboxes.js b/plugins/jetpack/modules/custom-post-types/js/menu-checkboxes.js
new file mode 100644
index 00000000..601f1bd7
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/js/menu-checkboxes.js
@@ -0,0 +1,55 @@
+/* jshint onevar: false, smarttabs: true, devel: true */
+
+( function( $ ) {
+ var NovaCheckBoxes = {
+ inputs: null,
+ popInputs: null,
+
+ initialize: function() {
+ NovaCheckBoxes.popInputs = $( '#nova_menuchecklist-pop' ).find( ':checkbox' );
+
+ NovaCheckBoxes.inputs = $( '#nova_menuchecklist' )
+ .find( ':checkbox' )
+ .change( NovaCheckBoxes.checkOne )
+ .change( NovaCheckBoxes.syncPop );
+
+ if ( ! NovaCheckBoxes.isChecked() ) {
+ NovaCheckBoxes.checkFirst();
+ }
+
+ NovaCheckBoxes.syncPop();
+ },
+
+ syncPop: function() {
+ NovaCheckBoxes.popInputs.each( function() {
+ var $this = $( this );
+ $this.prop( 'checked', $( '#in-nova_menu-' + $this.val() ).is( ':checked' ) );
+ } );
+ },
+
+ isChecked: function() {
+ return NovaCheckBoxes.inputs.is( ':checked' );
+ },
+
+ checkFirst: function() {
+ NovaCheckBoxes.inputs.first().prop( 'checked', true );
+ },
+
+ checkOne: function(/*event*/) {
+ if ( $( this ).is( ':checked' ) ) {
+ return NovaCheckBoxes.inputs.not( this ).prop( 'checked', false );
+ } else {
+ if (
+ $( this )
+ .closest( '#nova_menuchecklist' )
+ .find( ':checked' ).length > 0
+ ) {
+ return $( this ).prop( 'checked', false );
+ }
+ return NovaCheckBoxes.checkFirst();
+ }
+ },
+ };
+
+ $( NovaCheckBoxes.initialize );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/custom-post-types/js/nova-drag-drop.js b/plugins/jetpack/modules/custom-post-types/js/nova-drag-drop.js
new file mode 100644
index 00000000..d7291e96
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/js/nova-drag-drop.js
@@ -0,0 +1,67 @@
+/* jshint onevar: false, smarttabs: true */
+/* global _novaDragDrop */
+
+( function( $ ) {
+ var list;
+
+ function init() {
+ list = $( '#the-list' );
+ dragMenus();
+ addNonce();
+ addSubmitButton();
+ changeToPost();
+ }
+
+ function dragMenus() {
+ list.sortable( {
+ cancel: '.no-items',
+ stop: function( event, ui ) {
+ if ( ui.item.is( ':first-child' ) ) {
+ return list.sortable( 'cancel' );
+ }
+ //
+ reOrder();
+ },
+ } );
+ }
+
+ function reOrder() {
+ list.find( '.menu-label-row' ).each( function() {
+ var term_id = $( this ).data( 'term_id' );
+ $( this )
+ .nextUntil( '.menu-label-row' )
+ .each( function( i ) {
+ var row = $( this );
+ row.find( '.menu-order-value' ).val( i );
+ row.find( '.nova-menu-term' ).val( term_id );
+ } );
+ } );
+ }
+
+ function addSubmitButton() {
+ $( '.tablenav' ).prepend(
+ '<input type="submit" class="button-primary button-reorder alignright" value="' +
+ _novaDragDrop.reorder +
+ '" name="' +
+ _novaDragDrop.reorderName +
+ '" />'
+ );
+ }
+
+ function addNonce() {
+ $( '#posts-filter' ).append(
+ '<input type="hidden" name="' +
+ _novaDragDrop.nonceName +
+ '" value="' +
+ _novaDragDrop.nonce +
+ '" />'
+ );
+ }
+
+ function changeToPost() {
+ $( '#posts-filter' ).attr( 'method', 'post' );
+ }
+
+ // do it
+ $( document ).ready( init );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/custom-post-types/nova.php b/plugins/jetpack/modules/custom-post-types/nova.php
new file mode 100644
index 00000000..7301a314
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/nova.php
@@ -0,0 +1,1298 @@
+<?php
+
+/*
+ * Put the following code in your theme's Food Menu Page Template to customize the markup of the menu.
+
+if ( class_exists( 'Nova_Restaurant' ) ) {
+ Nova_Restaurant::init( array(
+ 'menu_tag' => 'section',
+ 'menu_class' => 'menu-items',
+ 'menu_header_tag' => 'header',
+ 'menu_header_class' => 'menu-group-header',
+ 'menu_title_tag' => 'h1',
+ 'menu_title_class' => 'menu-group-title',
+ 'menu_description_tag' => 'div',
+ 'menu_description_class' => 'menu-group-description',
+ ) );
+}
+
+*/
+
+/* @todo
+
+Bulk/Quick edit response of Menu Item rows is broken.
+
+Drag and Drop reordering.
+*/
+
+class Nova_Restaurant {
+ const MENU_ITEM_POST_TYPE = 'nova_menu_item';
+ const MENU_ITEM_LABEL_TAX = 'nova_menu_item_label';
+ const MENU_TAX = 'nova_menu';
+
+ public $version = '0.1';
+
+ protected $default_menu_item_loop_markup = array(
+ 'menu_tag' => 'section',
+ 'menu_class' => 'menu-items',
+ 'menu_header_tag' => 'header',
+ 'menu_header_class' => 'menu-group-header',
+ 'menu_title_tag' => 'h1',
+ 'menu_title_class' => 'menu-group-title',
+ 'menu_description_tag' => 'div',
+ 'menu_description_class' => 'menu-group-description',
+ );
+
+ protected $menu_item_loop_markup = array();
+ protected $menu_item_loop_last_term_id = false;
+ protected $menu_item_loop_current_term = false;
+
+ static function init( $menu_item_loop_markup = array() ) {
+ static $instance = false;
+
+ if ( !$instance ) {
+ $instance = new Nova_Restaurant;
+ }
+
+ if ( $menu_item_loop_markup ) {
+ $instance->menu_item_loop_markup = wp_parse_args( $menu_item_loop_markup, $instance->default_menu_item_loop_markup );
+ }
+
+ return $instance;
+ }
+
+ function __construct() {
+ if ( ! $this->site_supports_nova() )
+ return;
+
+ $this->register_taxonomies();
+ $this->register_post_types();
+ add_action( 'admin_menu', array( $this, 'add_admin_menus' ) );
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_nova_styles' ) );
+ add_action( 'admin_head', array( $this, 'set_custom_font_icon' ) );
+
+ // Always sort menu items correctly
+ add_action( 'parse_query', array( $this, 'sort_menu_item_queries_by_menu_order' ) );
+ add_filter( 'posts_results', array( $this, 'sort_menu_item_queries_by_menu_taxonomy' ), 10, 2 );
+
+ add_action( 'wp_insert_post', array( $this, 'add_post_meta' ) );
+
+ $this->menu_item_loop_markup = $this->default_menu_item_loop_markup;
+
+ // Only output our Menu Item Loop Markup on a real blog view. Not feeds, XML-RPC, admin, etc.
+ add_filter( 'template_include', array( $this, 'setup_menu_item_loop_markup__in_filter' ) );
+
+ add_filter( 'enter_title_here', array( $this, 'change_default_title' ) );
+ add_filter( 'post_updated_messages', array( $this, 'updated_messages' ) );
+ add_filter( 'dashboard_glance_items', array( $this, 'add_to_dashboard' ) );
+ }
+
+ /**
+ * Should this Custom Post Type be made available?
+ */
+ function site_supports_nova() {
+ // If we're on WordPress.com, and it has the menu site vertical.
+ if ( function_exists( 'site_vertical' ) && 'nova_menu' == site_vertical() )
+ return true;
+
+ // Else, if the current theme requests it.
+ if ( current_theme_supports( self::MENU_ITEM_POST_TYPE ) )
+ return true;
+
+ // Otherwise, say no unless something wants to filter us to say yes.
+ /**
+ * Allow something else to hook in and enable this CPT.
+ *
+ * @module custom-content-types
+ *
+ * @since 2.6.0
+ *
+ * @param bool false Whether or not to enable this CPT.
+ * @param string $var The slug for this CPT.
+ */
+ return (bool) apply_filters( 'jetpack_enable_cpt', false, self::MENU_ITEM_POST_TYPE );
+ }
+
+/* Setup */
+
+ /**
+ * Register Taxonomies and Post Type
+ */
+ function register_taxonomies() {
+ if ( ! taxonomy_exists( self::MENU_ITEM_LABEL_TAX ) ) {
+ register_taxonomy( self::MENU_ITEM_LABEL_TAX, self::MENU_ITEM_POST_TYPE, array(
+ 'labels' => array(
+ /* translators: this is about a food menu */
+ 'name' => __( 'Menu Item Labels', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'singular_name' => __( 'Menu Item Label', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'search_items' => __( 'Search Menu Item Labels', 'jetpack' ),
+ 'popular_items' => __( 'Popular Labels', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'all_items' => __( 'All Menu Item Labels', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'edit_item' => __( 'Edit Menu Item Label', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'view_item' => __( 'View Menu Item Label', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'update_item' => __( 'Update Menu Item Label', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'add_new_item' => __( 'Add New Menu Item Label', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'new_item_name' => __( 'New Menu Item Label Name', 'jetpack' ),
+ 'separate_items_with_commas' => __( 'For example, spicy, favorite, etc. <br /> Separate Labels with commas', 'jetpack' ),
+ 'add_or_remove_items' => __( 'Add or remove Labels', 'jetpack' ),
+ 'choose_from_most_used' => __( 'Choose from the most used Labels', 'jetpack' ),
+ 'items_list_navigation' => __( 'Menu item label list navigation', 'jetpack' ),
+ 'items_list' => __( 'Menu item labels list', 'jetpack' ),
+ ),
+ 'no_tagcloud' => __( 'No Labels found', 'jetpack' ),
+ 'hierarchical' => false,
+ ) );
+ }
+
+ if ( ! taxonomy_exists( self::MENU_TAX ) ) {
+ register_taxonomy( self::MENU_TAX, self::MENU_ITEM_POST_TYPE, array(
+ 'labels' => array(
+ /* translators: this is about a food menu */
+ 'name' => __( 'Menu Sections', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'singular_name' => __( 'Menu Section', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'search_items' => __( 'Search Menu Sections', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'all_items' => __( 'All Menu Sections', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'parent_item' => __( 'Parent Menu Section', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'parent_item_colon' => __( 'Parent Menu Section:', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'edit_item' => __( 'Edit Menu Section', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'view_item' => __( 'View Menu Section', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'update_item' => __( 'Update Menu Section', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'add_new_item' => __( 'Add New Menu Section', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'new_item_name' => __( 'New Menu Sections Name', 'jetpack' ),
+ 'items_list_navigation' => __( 'Menu section list navigation', 'jetpack' ),
+ 'items_list' => __( 'Menu section list', 'jetpack' ),
+ ),
+ 'rewrite' => array(
+ 'slug' => 'menu',
+ 'with_front' => false,
+ 'hierarchical' => true,
+ ),
+ 'hierarchical' => true,
+ 'show_tagcloud' => false,
+ 'query_var' => 'menu',
+ ) );
+ }
+ }
+
+ function register_post_types() {
+ if ( post_type_exists( self::MENU_ITEM_POST_TYPE ) ) {
+ return;
+ }
+
+ register_post_type( self::MENU_ITEM_POST_TYPE, array(
+ 'description' => __( "Items on your restaurant's menu", 'jetpack' ),
+
+ 'labels' => array(
+ /* translators: this is about a food menu */
+ 'name' => __( 'Menu Items', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'singular_name' => __( 'Menu Item', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'menu_name' => __( 'Food Menus', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'all_items' => __( 'Menu Items', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'add_new' => __( 'Add One Item', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'add_new_item' => __( 'Add Menu Item', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'edit_item' => __( 'Edit Menu Item', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'new_item' => __( 'New Menu Item', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'view_item' => __( 'View Menu Item', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'search_items' => __( 'Search Menu Items', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'not_found' => __( 'No Menu Items found', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 'not_found_in_trash' => __( 'No Menu Items found in Trash', 'jetpack' ),
+ 'filter_items_list' => __( 'Filter menu items list', 'jetpack' ),
+ 'items_list_navigation' => __( 'Menu item list navigation', 'jetpack' ),
+ 'items_list' => __( 'Menu items list', 'jetpack' ),
+ ),
+ 'supports' => array(
+ 'title',
+ 'editor',
+ 'thumbnail',
+ 'excerpt',
+ ),
+ 'rewrite' => array(
+ 'slug' => 'item',
+ 'with_front' => false,
+ 'feeds' => false,
+ 'pages' => false,
+ ),
+ 'register_meta_box_cb' => array( $this, 'register_menu_item_meta_boxes' ),
+
+ 'public' => true,
+ 'show_ui' => true, // set to false to replace with custom UI
+ 'menu_position' => 20, // below Pages
+ 'capability_type' => 'page',
+ 'map_meta_cap' => true,
+ 'has_archive' => false,
+ 'query_var' => 'item',
+ ) );
+ }
+
+
+ /**
+ * Update messages for the Menu Item admin.
+ */
+ function updated_messages( $messages ) {
+ global $post;
+
+ $messages[self::MENU_ITEM_POST_TYPE] = array(
+ 0 => '', // Unused. Messages start at index 1.
+ /* translators: this is about a food menu */
+ 1 => sprintf( __( 'Menu item updated. <a href="%s">View item</a>', 'jetpack' ), esc_url( get_permalink( $post->ID ) ) ),
+ 2 => esc_html__( 'Custom field updated.', 'jetpack' ),
+ 3 => esc_html__( 'Custom field deleted.', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 4 => esc_html__( 'Menu item updated.', 'jetpack' ),
+ /* translators: %s: date and time of the revision */
+ 5 => isset( $_GET['revision'] ) ? sprintf( esc_html__( 'Menu item restored to revision from %s', 'jetpack' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
+ /* translators: this is about a food menu */
+ 6 => sprintf( __( 'Menu item published. <a href="%s">View item</a>', 'jetpack' ), esc_url( get_permalink( $post->ID ) ) ),
+ /* translators: this is about a food menu */
+ 7 => esc_html__( 'Menu item saved.', 'jetpack' ),
+ /* translators: this is about a food menu */
+ 8 => sprintf( __( 'Menu item submitted. <a target="_blank" href="%s">Preview item</a>', 'jetpack' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ),
+ /* translators: this is about a food menu */
+ 9 => sprintf( __( 'Menu item scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview item</a>', 'jetpack' ),
+ // translators: Publish box date format, see http://php.net/date
+ date_i18n( __( 'M j, Y @ G:i', 'jetpack' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post->ID) ) ),
+ /* translators: this is about a food menu */
+ 10 => sprintf( __( 'Menu item draft updated. <a target="_blank" href="%s">Preview item</a>', 'jetpack' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ),
+ );
+
+ return $messages;
+ }
+
+
+ /**
+ * Nova Styles and Scripts
+ */
+ function enqueue_nova_styles( $hook ) {
+ global $post_type;
+ $pages = array( 'edit.php', 'post.php', 'post-new.php' );
+
+ if ( in_array( $hook, $pages ) && $post_type == self::MENU_ITEM_POST_TYPE ) {
+ wp_enqueue_style( 'nova-style', plugins_url( 'css/nova.css', __FILE__ ), array(), $this->version );
+ }
+
+ wp_enqueue_style( 'nova-font', plugins_url( 'css/nova-font.css', __FILE__ ), array(), $this->version );
+ }
+
+
+ /**
+ * Change ‘Enter Title Here’ text for the Menu Item.
+ */
+ function change_default_title( $title ) {
+ if ( self::MENU_ITEM_POST_TYPE == get_post_type() ) {
+ /* translators: this is about a food menu */
+ $title = esc_html__( "Enter the menu item's name here", 'jetpack' );
+ }
+
+ return $title;
+ }
+
+
+ /**
+ * Add to Dashboard At A Glance
+ */
+ function add_to_dashboard() {
+ $number_menu_items = wp_count_posts( self::MENU_ITEM_POST_TYPE );
+
+ if ( current_user_can( 'administrator' ) ) {
+ $number_menu_items_published = sprintf( '<a href="%1$s">%2$s</a>',
+ esc_url( get_admin_url( get_current_blog_id(), 'edit.php?post_type=' . self::MENU_ITEM_POST_TYPE ) ),
+ sprintf( _n( '%1$d Food Menu Item', '%1$d Food Menu Items', intval( $number_menu_items->publish ), 'jetpack' ), number_format_i18n( $number_menu_items->publish ) )
+ );
+ }
+ else {
+ $number_menu_items_published = sprintf( '<span>%1$s</span>',
+ sprintf( _n( '%1$d Food Menu Item', '%1$d Food Menu Items', intval( $number_menu_items->publish ), 'jetpack' ), number_format_i18n( $number_menu_items->publish ) )
+ );
+ }
+
+ echo '<li class="nova-menu-count">' . $number_menu_items_published . '</li>';
+ }
+
+
+ /**
+ * Query
+ */
+ function is_menu_item_query( $query ) {
+ if (
+ ( isset( $query->query_vars['taxonomy'] ) && self::MENU_TAX == $query->query_vars['taxonomy'] )
+ ||
+ ( isset( $query->query_vars['post_type'] ) && self::MENU_ITEM_POST_TYPE == $query->query_vars['post_type'] )
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function sort_menu_item_queries_by_menu_order( $query ) {
+ if ( ! $this->is_menu_item_query( $query ) ) {
+ return;
+ }
+
+ $query->query_vars['orderby'] = 'menu_order';
+ $query->query_vars['order'] = 'ASC';
+
+ // For now, just turn off paging so we can sort by taxonmy later
+ // If we want paging in the future, we'll need to add the taxonomy sort here (or at least before the DB query is made)
+ $query->query_vars['posts_per_page'] = -1;
+ }
+
+ function sort_menu_item_queries_by_menu_taxonomy( $posts, $query ) {
+ if ( !$posts ) {
+ return $posts;
+ }
+
+ if ( !$this->is_menu_item_query( $query ) ) {
+ return $posts;
+ }
+
+ $grouped_by_term = array();
+
+ foreach ( $posts as $post ) {
+ $term = $this->get_menu_item_menu_leaf( $post->ID );
+ if ( !$term || is_wp_error( $term ) ) {
+ $term_id = 0;
+ } else {
+ $term_id = $term->term_id;
+ }
+
+ if ( !isset( $grouped_by_term["$term_id"] ) ) {
+ $grouped_by_term["$term_id"] = array();
+ }
+
+ $grouped_by_term["$term_id"][] = $post;
+ }
+
+ $term_order = get_option( 'nova_menu_order', array() );
+
+ $return = array();
+ foreach ( $term_order as $term_id ) {
+ if ( isset( $grouped_by_term["$term_id"] ) ) {
+ $return = array_merge( $return, $grouped_by_term["$term_id"] );
+ unset( $grouped_by_term["$term_id"] );
+ }
+ }
+
+ foreach ( $grouped_by_term as $term_id => $posts ) {
+ $return = array_merge( $return, $posts );
+ }
+
+ return $return;
+ }
+
+
+ /**
+ * Add Many Items
+ */
+ function add_admin_menus() {
+ $hook = add_submenu_page(
+ 'edit.php?post_type=' . self::MENU_ITEM_POST_TYPE,
+ __( 'Add Many Items', 'jetpack' ),
+ __( 'Add Many Items', 'jetpack' ),
+ 'edit_pages',
+ 'add_many_nova_items',
+ array( $this, 'add_many_new_items_page' )
+ );
+
+ add_action( "load-$hook", array( $this, 'add_many_new_items_page_load' ) );
+
+ add_action( 'current_screen', array( $this, 'current_screen_load' ) );
+
+ //Adjust 'Add Many Items' submenu position
+ if ( isset( $GLOBALS['submenu']['edit.php?post_type=' . self::MENU_ITEM_POST_TYPE] ) ) {
+ $submenu_item = array_pop( $GLOBALS['submenu']['edit.php?post_type=' . self::MENU_ITEM_POST_TYPE] );
+ $GLOBALS['submenu']['edit.php?post_type=' . self::MENU_ITEM_POST_TYPE][11] = $submenu_item;
+ ksort( $GLOBALS['submenu']['edit.php?post_type=' . self::MENU_ITEM_POST_TYPE] );
+ }
+
+
+ $this->setup_menu_item_columns();
+
+ wp_register_script(
+ 'nova-menu-checkboxes',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/custom-post-types/js/menu-checkboxes.min.js',
+ 'modules/custom-post-types/js/menu-checkboxes.js'
+ ),
+ array( 'jquery' ),
+ $this->version,
+ true
+ );
+ }
+
+
+ /**
+ * Custom Nova Icon CSS
+ */
+ function set_custom_font_icon() {
+ ?>
+ <style type="text/css">
+ #menu-posts-nova_menu_item .wp-menu-image:before {
+ font-family: 'nova-font' !important;
+ content: '\e603' !important;
+ }
+ </style>
+ <?php
+ }
+
+ function current_screen_load() {
+ $screen = get_current_screen();
+ if ( 'edit-nova_menu_item' !== $screen->id ) {
+ return;
+ }
+
+ $this->edit_menu_items_page_load();
+ add_filter( 'admin_notices', array( $this, 'admin_notices' ) );
+ }
+
+/* Edit Items List */
+
+ function admin_notices() {
+ if ( isset( $_GET['nova_reordered'] ) )
+ /* translators: this is about a food menu */
+ printf( '<div class="updated"><p>%s</p></div>', __( 'Menu Items re-ordered.', 'jetpack' ) );
+ }
+
+ function no_title_sorting( $columns ) {
+ if ( isset( $columns['title'] ) )
+ unset( $columns['title'] );
+ return $columns;
+ }
+
+ function setup_menu_item_columns() {
+ add_filter( sprintf( 'manage_edit-%s_sortable_columns', self::MENU_ITEM_POST_TYPE ), array( $this, 'no_title_sorting' ) );
+ add_filter( sprintf( 'manage_%s_posts_columns', self::MENU_ITEM_POST_TYPE ), array( $this, 'menu_item_columns' ) );
+
+ add_action( sprintf( 'manage_%s_posts_custom_column', self::MENU_ITEM_POST_TYPE ), array( $this, 'menu_item_column_callback' ), 10, 2 );
+ }
+
+ function menu_item_columns( $columns ) {
+ unset( $columns['date'], $columns['likes'] );
+
+ $columns['thumbnail'] = __( 'Thumbnail', 'jetpack' );
+ $columns['labels'] = __( 'Labels', 'jetpack' );
+ $columns['price'] = __( 'Price', 'jetpack' );
+ $columns['order'] = __( 'Order', 'jetpack' );
+
+ return $columns;
+ }
+
+ function menu_item_column_callback( $column, $post_id ) {
+ $screen = get_current_screen();
+
+ switch ( $column ) {
+ case 'thumbnail':
+ echo get_the_post_thumbnail( $post_id, array( 50, 50 ) );
+ break;
+ case 'labels' :
+ $this->list_admin_labels( $post_id );
+ break;
+ case 'price' :
+ $this->display_price( $post_id );
+ break;
+ case 'order' :
+ $url = admin_url( $screen->parent_file );
+
+ $up_url = add_query_arg( array(
+ 'action' => 'move-item-up',
+ 'post_id' => (int) $post_id,
+ ), wp_nonce_url( $url, 'nova_move_item_up_' . $post_id ) );
+
+ $down_url = add_query_arg( array(
+ 'action' => 'move-item-down',
+ 'post_id' => (int) $post_id,
+ ), wp_nonce_url( $url, 'nova_move_item_down_' . $post_id ) );
+ $menu_item = get_post($post_id);
+ $this->get_menu_by_post_id( $post_id );
+ if ( $term_id = $this->get_menu_by_post_id( $post_id ) ) {
+ $term_id = $term_id->term_id;
+ }
+ ?>
+ <input type="hidden" class="menu-order-value" name="nova_order[<?php echo (int) $post_id ?>]" value="<?php echo esc_attr( $menu_item->menu_order ) ?>" />
+ <input type="hidden" class='nova-menu-term' name="nova_menu_term[<?php echo (int) $post_id ?>]" value="<?php echo esc_attr( $term_id ); ?>">
+
+ <span class="hide-if-js">
+ &nbsp; &nbsp; &mdash; <a class="nova-move-item-up" data-post-id="<?php echo (int) $post_id; ?>" href="<?php echo esc_url( $up_url ); ?>">up</a>
+ <br />
+ &nbsp; &nbsp; &mdash; <a class="nova-move-item-down" data-post-id="<?php echo (int) $post_id; ?>" href="<?php echo esc_url( $down_url ); ?>">down</a>
+ </span>
+ <?php
+ break;
+ }
+ }
+
+ function get_menu_by_post_id( $post_id = null ) {
+ if ( ! $post_id )
+ return false;
+
+ $terms = get_the_terms( $post_id, self::MENU_TAX );
+
+ if ( ! is_array( $terms ) )
+ return false;
+
+ return array_pop( $terms );
+ }
+
+ /**
+ * Fires on a menu edit page. We might have drag-n-drop reordered
+ */
+ function maybe_reorder_menu_items() {
+ // make sure we clicked our button
+ if ( ! ( isset( $_REQUEST['menu_reorder_submit'] ) && $_REQUEST['menu_reorder_submit'] === __( 'Save New Order', 'jetpack' ) ) )
+ return;
+ ;
+
+ // make sure we have the nonce
+ if ( ! ( isset( $_REQUEST['drag-drop-reorder'] ) && wp_verify_nonce( $_REQUEST['drag-drop-reorder'], 'drag-drop-reorder' ) ) )
+ return;
+
+ $term_pairs = array_map( 'absint', $_REQUEST['nova_menu_term'] );
+ $order_pairs = array_map( 'absint', $_REQUEST['nova_order'] );
+
+ foreach( $order_pairs as $ID => $menu_order ) {
+ $ID = absint( $ID );
+ unset( $order_pairs[$ID] );
+ if ( $ID < 0 )
+ continue;
+
+ $post = get_post( $ID );
+ if ( ! $post )
+ continue;
+
+ // save a write if the order hasn't changed
+ if ( $menu_order != $post->menu_order )
+ wp_update_post( compact( 'ID', 'menu_order' ) );
+
+ // save a write if the term hasn't changed
+ if ( $term_pairs[$ID] != $this->get_menu_by_post_id( $ID )->term_id )
+ wp_set_object_terms( $ID, $term_pairs[$ID], self::MENU_TAX );
+
+ }
+
+ $redirect = add_query_arg( array(
+ 'post_type' => self::MENU_ITEM_POST_TYPE,
+ 'nova_reordered' => '1'
+ ), admin_url( 'edit.php' ) );
+ wp_safe_redirect( $redirect );
+ exit;
+
+ }
+
+ function edit_menu_items_page_load() {
+ if ( isset( $_GET['action'] ) ) {
+ $this->handle_menu_item_actions();
+ }
+
+ $this->maybe_reorder_menu_items();
+
+ wp_enqueue_script(
+ 'nova-drag-drop',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/custom-post-types/js/nova-drag-drop.min.js',
+ 'modules/custom-post-types/js/nova-drag-drop.js'
+ ),
+ array( 'jquery-ui-sortable' ),
+ $this->version,
+ true
+ );
+
+ wp_localize_script( 'nova-drag-drop', '_novaDragDrop', array(
+ 'nonce' => wp_create_nonce( 'drag-drop-reorder' ),
+ 'nonceName' => 'drag-drop-reorder',
+ 'reorder' => __( 'Save New Order', 'jetpack' ),
+ 'reorderName' => 'menu_reorder_submit'
+ ) );
+ add_action( 'the_post', array( $this, 'show_menu_titles_in_menu_item_list' ) );
+ }
+
+ function handle_menu_item_actions() {
+ $action = (string) $_GET['action'];
+
+ switch ( $action ) {
+ case 'move-item-up' :
+ case 'move-item-down' :
+ $reorder = false;
+
+ $post_id = (int) $_GET['post_id'];
+
+ $term = $this->get_menu_item_menu_leaf( $post_id );
+
+ // Get all posts in that term
+ $query = new WP_Query( array(
+ 'taxonomy' => self::MENU_TAX,
+ 'term' => $term->slug,
+ ) );
+
+ $order = array();
+ foreach ( $query->posts as $post ) {
+ $order[] = $post->ID;
+ }
+
+ if ( 'move-item-up' == $action ) {
+ check_admin_referer( 'nova_move_item_up_' . $post_id );
+
+ $first_post_id = $order[0];
+ if ( $post_id == $first_post_id ) {
+ break;
+ }
+
+ foreach ( $order as $menu_order => $order_post_id ) {
+ if ( $post_id != $order_post_id ) {
+ continue;
+ }
+
+ $swap_post_id = $order[$menu_order - 1];
+ $order[$menu_order - 1] = $post_id;
+ $order[$menu_order] = $swap_post_id;
+
+ $reorder = true;
+ break;
+ }
+ } else {
+ check_admin_referer( 'nova_move_item_down_' . $post_id );
+
+ $last_post_id = end( $order );
+ if ( $post_id == $last_post_id ) {
+ break;
+ }
+
+ foreach ( $order as $menu_order => $order_post_id ) {
+ if ( $post_id != $order_post_id ) {
+ continue;
+ }
+
+ $swap_post_id = $order[$menu_order + 1];
+ $order[$menu_order + 1] = $post_id;
+ $order[$menu_order] = $swap_post_id;
+
+ $reorder = true;
+ }
+ }
+
+ if ( $reorder ) {
+ foreach ( $order as $menu_order => $ID ) {
+ wp_update_post( compact( 'ID', 'menu_order' ) );
+ }
+ }
+
+ break;
+ case 'move-menu-up' :
+ case 'move-menu-down' :
+ $reorder = false;
+
+ $term_id = (int) $_GET['term_id'];
+
+ $terms = $this->get_menus();
+
+ $order = array();
+ foreach ( $terms as $term ) {
+ $order[] = $term->term_id;
+ }
+
+ if ( 'move-menu-up' == $action ) {
+ check_admin_referer( 'nova_move_menu_up_' . $term_id );
+
+ $first_term_id = $order[0];
+ if ( $term_id == $first_term_id ) {
+ break;
+ }
+
+ foreach ( $order as $menu_order => $order_term_id ) {
+ if ( $term_id != $order_term_id ) {
+ continue;
+ }
+
+ $swap_term_id = $order[$menu_order - 1];
+ $order[$menu_order - 1] = $term_id;
+ $order[$menu_order] = $swap_term_id;
+
+ $reorder = true;
+ break;
+ }
+ } else {
+ check_admin_referer( 'nova_move_menu_down_' . $term_id );
+
+ $last_term_id = end( $order );
+ if ( $term_id == $last_term_id ) {
+ break;
+ }
+
+ foreach ( $order as $menu_order => $order_term_id ) {
+ if ( $term_id != $order_term_id ) {
+ continue;
+ }
+
+ $swap_term_id = $order[$menu_order + 1];
+ $order[$menu_order + 1] = $term_id;
+ $order[$menu_order] = $swap_term_id;
+
+ $reorder = true;
+ }
+ }
+
+ if ( $reorder ) {
+ update_option( 'nova_menu_order', $order );
+ }
+
+ break;
+ default :
+ return;
+ }
+
+ $redirect = add_query_arg( array(
+ 'post_type' => self::MENU_ITEM_POST_TYPE,
+ 'nova_reordered' => '1'
+ ), admin_url( 'edit.php' ) );
+ wp_safe_redirect( $redirect );
+ exit;
+ }
+
+ /*
+ * Add menu title rows to the list table
+ */
+ function show_menu_titles_in_menu_item_list( $post ) {
+ global $wp_list_table;
+
+ static $last_term_id = false;
+
+ $term = $this->get_menu_item_menu_leaf( $post->ID );
+
+ $term_id = $term instanceof WP_Term ? $term->term_id : null;
+
+ if ( false !== $last_term_id && $last_term_id === $term_id ) {
+ return;
+ }
+
+ if ( is_null( $term_id ) ) {
+ $last_term_id = null;
+ $term_name = '';
+ $parent_count = 0;
+ } else {
+ $last_term_id = $term->term_id;
+ $term_name = $term->name;
+ $parent_count = 0;
+ $current_term = $term;
+ while ( $current_term->parent ) {
+ $parent_count++;
+ $current_term = get_term( $current_term->parent, self::MENU_TAX );
+ }
+ }
+
+ $non_order_column_count = $wp_list_table->get_column_count() - 1;
+
+ $screen = get_current_screen();
+
+ $url = admin_url( $screen->parent_file );
+
+ $up_url = add_query_arg( array(
+ 'action' => 'move-menu-up',
+ 'term_id' => (int) $term_id,
+ ), wp_nonce_url( $url, 'nova_move_menu_up_' . $term_id ) );
+
+ $down_url = add_query_arg( array(
+ 'action' => 'move-menu-down',
+ 'term_id' => (int) $term_id,
+ ), wp_nonce_url( $url, 'nova_move_menu_down_' . $term_id ) );
+
+?>
+ <tr class="no-items menu-label-row" data-term_id="<?php echo esc_attr( $term_id ) ?>">
+ <td class="colspanchange" colspan="<?php echo (int) $non_order_column_count; ?>">
+ <h3><?php
+ echo str_repeat( ' &mdash; ', (int) $parent_count );
+
+ if ( $term instanceof WP_Term ) {
+ echo esc_html( sanitize_term_field( 'name', $term_name, $term_id, self::MENU_TAX, 'display' ) );
+ edit_term_link( __( 'edit', 'jetpack' ), '<span class="edit-nova-section"><span class="dashicon dashicon-edit"></span>', '</span>', $term );
+
+ } else {
+ _e( 'Uncategorized' , 'jetpack' );
+ }
+ ?></h3>
+ </td>
+ <td>
+ <?php if ( $term instanceof WP_Term ) { ?>
+ <a class="nova-move-menu-up" title="<?php esc_attr_e( 'Move menu section up', 'jetpack' ); ?>" href="<?php echo esc_url( $up_url ); ?>"><?php esc_html_e( 'UP', 'jetpack' ); ?></a>
+ <br />
+ <a class="nova-move-menu-down" title="<?php esc_attr_e( 'Move menu section down', 'jetpack' ); ?>" href="<?php echo esc_url( $down_url ); ?>"><?php esc_html_e( 'DOWN', 'jetpack' ); ?></a>
+ <?php } ?>
+ </td>
+ </tr>
+<?php
+ }
+
+/* Edit Many Items */
+
+ function add_many_new_items_page_load() {
+ if ( 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) {
+ $this->process_form_request();
+ exit;
+ }
+
+ $this->enqueue_many_items_scripts();
+ }
+
+ function enqueue_many_items_scripts() {
+ wp_enqueue_script(
+ 'nova-many-items',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/custom-post-types/js/many-items.min.js',
+ 'modules/custom-post-types/js/many-items.js'
+ ),
+ array( 'jquery' ),
+ $this->version,
+ true
+ );
+ }
+
+ function process_form_request() {
+ if ( !isset( $_POST['nova_title'] ) || !is_array( $_POST['nova_title'] ) ) {
+ return;
+ }
+
+ $is_ajax = !empty( $_POST['ajax'] );
+
+ if ( $is_ajax ) {
+ check_ajax_referer( 'nova_many_items' );
+ } else {
+ check_admin_referer( 'nova_many_items' );
+ }
+
+ foreach ( array_keys( $_POST['nova_title'] ) as $key ) :
+ // $_POST is already slashed
+ $post_details = array(
+ 'post_status' => 'publish',
+ 'post_type' => self::MENU_ITEM_POST_TYPE,
+ 'post_content' => $_POST['nova_content'][$key],
+ 'post_title' => $_POST['nova_title'][$key],
+ 'tax_input' => array(
+ self::MENU_ITEM_LABEL_TAX => $_POST['nova_labels'][$key],
+ self::MENU_TAX => isset( $_POST['nova_menu_tax'] ) ? $_POST['nova_menu_tax'] : null,
+ ),
+ );
+
+ $post_id = wp_insert_post( $post_details );
+ if ( !$post_id || is_wp_error( $post_id ) ) {
+ continue;
+ }
+
+ $this->set_price( $post_id, isset( $_POST['nova_price'][$key] ) ? stripslashes( $_POST['nova_price'][$key] ) : '' );
+
+ if ( $is_ajax ) :
+ $post = get_post( $post_id );
+ $GLOBALS['post'] = $post;
+ setup_postdata( $post );
+
+?>
+ <td><?php the_title(); ?></td>
+ <td class="nova-price"><?php $this->display_price(); ?></td>
+ <td><?php $this->list_labels( $post_id ); ?></td>
+ <td><?php the_content(); ?></td>
+<?php
+ endif;
+
+ endforeach;
+
+ if ( $is_ajax ) {
+ exit;
+ }
+
+ wp_safe_redirect( admin_url( 'edit.php?post_type=' . self::MENU_ITEM_POST_TYPE ) );
+ exit;
+ }
+
+ function add_many_new_items_page() {
+?>
+ <div class="wrap">
+ <h2><?php esc_html_e( 'Add Many Items', 'jetpack' ); ?></h2>
+
+ <p><?php _e( 'Use the <kbd>TAB</kbd> key on your keyboard to move between colums and the <kbd>ENTER</kbd> or <kbd>RETURN</kbd> key to save each row and move on to the next.', 'jetpack' ); ?></p>
+
+ <form method="post" action="" enctype="multipart/form-data">
+ <p><h3><?php esc_html_e( 'Add to section:', 'jetpack' ); ?> <?php wp_dropdown_categories( array(
+ 'id' => 'nova-menu-tax',
+ 'name' => 'nova_menu_tax',
+ 'taxonomy' => self::MENU_TAX,
+ 'hide_empty' => false,
+ 'hierarchical' => true,
+ ) ); ?></h3></p>
+
+ <table class="many-items-table wp-list-table widefat">
+ <thead>
+ <tr>
+ <th scope="col"><?php esc_html_e( 'Name', 'jetpack' ); ?></th>
+ <th scope="col" class="nova-price"><?php esc_html_e( 'Price', 'jetpack' ); ?></th>
+ <th scope="col"><?php _e( 'Labels: <small>spicy, favorite, etc. <em>Separate Labels with commas</em></small>', 'jetpack' ); ?></th>
+ <th scope="col"><?php esc_html_e( 'Description', 'jetpack' ); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><input type="text" name="nova_title[]" aria-required="true" /></td>
+ <td class="nova-price"><input type="text" name="nova_price[]" /></td>
+ <td><input type="text" name="nova_labels[]" /></td>
+ <td><textarea name="nova_content[]" cols="20" rows="1"></textarea>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <td><input type="text" name="nova_title[]" aria-required="true" /></td>
+ <td class="nova-price"><input type="text" name="nova_price[]" /></td>
+ <td><input type="text" name="nova_labels[]" /></td>
+ <td><textarea name="nova_content[]" cols="20" rows="1"></textarea>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <th><a class="button button-secondary nova-new-row"><span class="dashicon dashicon-plus"></span> <?php esc_html_e( 'New Row' , 'jetpack' ); ?></a></th>
+ <th class="nova-price"></th>
+ <th></th>
+ <th></th>
+ </tr>
+ </tfoot>
+ </table>
+
+ <p class="submit">
+ <input type="submit" class="button-primary" value="<?php esc_attr_e( 'Add These New Menu Items', 'jetpack' ); ?>" />
+ <?php wp_nonce_field( 'nova_many_items' ); ?>
+ </p>
+ </form>
+ </div>
+<?php
+ }
+
+/* Edit One Item */
+
+ function register_menu_item_meta_boxes() {
+ wp_enqueue_script( 'nova-menu-checkboxes' );
+
+ add_meta_box( 'menu_item_price', __( 'Price', 'jetpack' ), array( $this, 'menu_item_price_meta_box' ), null, 'side', 'high' );
+ }
+
+ function menu_item_price_meta_box( $post, $meta_box ) {
+ $price = $this->get_price( $post->ID );
+?>
+ <label for="nova-price-<?php echo (int) $post->ID; ?>" class="screen-reader-text"><?php esc_html_e( 'Price', 'jetpack' ); ?></label>
+ <input type="text" id="nova-price-<?php echo (int) $post->ID; ?>" class="widefat" name="nova_price[<?php echo (int) $post->ID; ?>]" value="<?php echo esc_attr( $price ); ?>" />
+<?php
+ }
+
+ function add_post_meta( $post_id ) {
+ if ( !isset( $_POST['nova_price'][$post_id] ) ) {
+ return;
+ }
+
+ $this->set_price( $post_id, stripslashes( $_POST['nova_price'][$post_id] ) );
+ }
+
+/* Data */
+
+ function get_menus( $args = array() ) {
+ $args = wp_parse_args( $args, array(
+ 'hide_empty' => false,
+ ) );
+
+ $terms = get_terms( self::MENU_TAX, $args );
+ if ( !$terms || is_wp_error( $terms ) ) {
+ return array();
+ }
+
+ $terms_by_id = array();
+ foreach ( $terms as $term ) {
+ $terms_by_id["{$term->term_id}"] = $term;
+ }
+
+ $term_order = get_option( 'nova_menu_order', array() );
+
+ $return = array();
+ foreach ( $term_order as $term_id ) {
+ if ( isset( $terms_by_id["$term_id"] ) ) {
+ $return[] = $terms_by_id["$term_id"];
+ unset( $terms_by_id["$term_id"] );
+ }
+ }
+
+ foreach ( $terms_by_id as $term_id => $term ) {
+ $return[] = $term;
+ }
+
+ return $return;
+ }
+
+ function get_menu_item_menu_leaf( $post_id ) {
+ // Get first menu taxonomy "leaf"
+ $term_ids = wp_get_object_terms( $post_id, self::MENU_TAX, array( 'fields' => 'ids' ) );
+
+ foreach ( $term_ids as $term_id ) {
+ $children = get_term_children( $term_id, self::MENU_TAX );
+ if ( ! $children ) {
+ break;
+ }
+ }
+
+ if ( ! isset( $term_id ) ) {
+ return false;
+ }
+
+ return get_term( $term_id, self::MENU_TAX );
+
+ }
+
+ function list_labels( $post_id = 0 ) {
+ $post = get_post( $post_id );
+ echo get_the_term_list( $post->ID, self::MENU_ITEM_LABEL_TAX, '', _x( ', ', 'Nova label separator', 'jetpack' ), '' );
+ }
+
+ function list_admin_labels( $post_id = 0 ) {
+ $post = get_post( $post_id );
+ $labels = get_the_terms( $post->ID, self::MENU_ITEM_LABEL_TAX );
+ if ( !empty( $labels ) ) {
+ $out = array();
+ foreach ( $labels as $label ) {
+ $out[] = sprintf( '<a href="%s">%s</a>',
+ esc_url( add_query_arg( array(
+ 'post_type' => self::MENU_ITEM_POST_TYPE,
+ 'taxonomy' => self::MENU_ITEM_LABEL_TAX,
+ 'term' => $label->slug
+ ), 'edit.php' ) ),
+ esc_html( sanitize_term_field( 'name', $label->name, $label->term_id, self::MENU_ITEM_LABEL_TAX, 'display' ) )
+ );
+ }
+
+ echo join( _x( ', ', 'Nova label separator', 'jetpack' ), $out );
+ } else {
+ esc_html_e( 'No Labels', 'jetpack' );
+ }
+ }
+
+ function set_price( $post_id = 0, $price = '' ) {
+ $post = get_post( $post_id );
+
+ return update_post_meta( $post->ID, 'nova_price', $price );
+ }
+
+ function get_price( $post_id = 0 ) {
+ $post = get_post( $post_id );
+
+ return get_post_meta( $post->ID, 'nova_price', true );
+ }
+
+ function display_price( $post_id = 0 ) {
+ echo esc_html( $this->get_price( $post_id ) );
+ }
+
+/* Menu Item Loop Markup */
+
+ /* Does not support nested loops */
+
+ function get_menu_item_loop_markup( $field = null ) {
+ return $this->menu_item_loop_markup;
+ }
+
+ /**
+ * Sets up the loop markup.
+ * Attached to the 'template_include' *filter*,
+ * which fires only during a real blog view (not in admin, feeds, etc.)
+ *
+ * @param string Template File
+ * @return string Template File. VERY Important.
+ */
+ function setup_menu_item_loop_markup__in_filter( $template ) {
+ add_action( 'loop_start', array( $this, 'start_menu_item_loop' ) );
+
+ return $template;
+ }
+
+ /**
+ * If the Query is a Menu Item Query, start outputing the Menu Item Loop Marku
+ * Attached to the 'loop_start' action.
+ *
+ * @param WP_Query
+ */
+ function start_menu_item_loop( $query ) {
+ if ( !$this->is_menu_item_query( $query ) ) {
+ return;
+ }
+
+ $this->menu_item_loop_last_term_id = false;
+ $this->menu_item_loop_current_term = false;
+
+ add_action( 'the_post', array( $this, 'menu_item_loop_each_post' ) );
+ add_action( 'loop_end', array( $this, 'stop_menu_item_loop' ) );
+ }
+
+ /**
+ * Outputs the Menu Item Loop Marku
+ * Attached to the 'the_post' action.
+ *
+ * @param WP_Post
+ */
+ function menu_item_loop_each_post( $post ) {
+ $this->menu_item_loop_current_term = $this->get_menu_item_menu_leaf( $post->ID );
+
+ if ( false === $this->menu_item_loop_last_term_id ) {
+ // We're at the very beginning of the loop
+
+ $this->menu_item_loop_open_element( 'menu' ); // Start a new menu section
+ $this->menu_item_loop_header(); // Output the menu's header
+ } elseif ( $this->menu_item_loop_last_term_id != $this->menu_item_loop_current_term->term_id ) {
+ // We're not at the very beginning but still need to start a new menu section. End the previous menu section first.
+
+ $this->menu_item_loop_close_element( 'menu' ); // End the previous menu section
+ $this->menu_item_loop_open_element( 'menu' ); // Start a new menu section
+ $this->menu_item_loop_header(); // Output the menu's header
+ }
+
+ $this->menu_item_loop_last_term_id = $this->menu_item_loop_current_term->term_id;
+ }
+
+ /**
+ * If the Query is a Menu Item Query, stop outputing the Menu Item Loop Marku
+ * Attached to the 'loop_end' action.
+ *
+ * @param WP_Query
+ */
+ function stop_menu_item_loop( $query ) {
+ if ( !$this->is_menu_item_query( $query ) ) {
+ return;
+ }
+
+ remove_action( 'the_post', array( $this, 'menu_item_loop_each_post' ) );
+ remove_action( 'loop_start', array( $this, 'start_menu_item_loop' ) );
+ remove_action( 'loop_end', array( $this, 'stop_menu_item_loop' ) );
+
+ $this->menu_item_loop_close_element( 'menu' ); // End the last menu section
+ }
+
+ /**
+ * Outputs the Menu Group Header
+ */
+ function menu_item_loop_header() {
+ $this->menu_item_loop_open_element( 'menu_header' );
+ $this->menu_item_loop_open_element( 'menu_title' );
+ echo esc_html( $this->menu_item_loop_current_term->name ); // @todo tax filter
+ $this->menu_item_loop_close_element( 'menu_title' );
+ if ( $this->menu_item_loop_current_term->description ) :
+ $this->menu_item_loop_open_element( 'menu_description' );
+ echo esc_html( $this->menu_item_loop_current_term->description ); // @todo kses, tax filter
+ $this->menu_item_loop_close_element( 'menu_description' );
+ endif;
+ $this->menu_item_loop_close_element( 'menu_header' );
+ }
+
+ /**
+ * Outputs a Menu Item Markup element opening tag
+ *
+ * @param string $field - Menu Item Markup settings field.
+ */
+ function menu_item_loop_open_element( $field ) {
+ $markup = $this->get_menu_item_loop_markup();
+ /**
+ * Filter a menu item's element opening tag.
+ *
+ * @module custom-content-types
+ *
+ * @since 4.4.0
+ *
+ * @param string $tag Menu item's element opening tag.
+ * @param string $field Menu Item Markup settings field.
+ * @param array $markup Array of markup elements for the menu item.
+ * @param false|object $term Taxonomy term for current menu item.
+ */
+ echo apply_filters(
+ 'jetpack_nova_menu_item_loop_open_element',
+ '<' . tag_escape( $markup["{$field}_tag"] ) . $this->menu_item_loop_class( $markup["{$field}_class"] ) . ">\n",
+ $field,
+ $markup,
+ $this->menu_item_loop_current_term
+ );
+ }
+
+ /**
+ * Outputs a Menu Item Markup element closing tag
+ *
+ * @param string $field - Menu Item Markup settings field
+ */
+ function menu_item_loop_close_element( $field ) {
+ $markup = $this->get_menu_item_loop_markup();
+ /**
+ * Filter a menu item's element closing tag.
+ *
+ * @module custom-content-types
+ *
+ * @since 4.4.0
+ *
+ * @param string $tag Menu item's element closing tag.
+ * @param string $field Menu Item Markup settings field.
+ * @param array $markup Array of markup elements for the menu item.
+ * @param false|object $term Taxonomy term for current menu item.
+ */
+ echo apply_filters(
+ 'jetpack_nova_menu_item_loop_close_element',
+ '</' . tag_escape( $markup["{$field}_tag"] ) . ">\n",
+ $field,
+ $markup,
+ $this->menu_item_loop_current_term
+ );
+ }
+
+ /**
+ * Returns a Menu Item Markup element's class attribute.
+ *
+ * @param string $class Class name.
+ * @return string HTML class attribute with leading whitespace.
+ */
+ function menu_item_loop_class( $class ) {
+ if ( ! $class ) {
+ return '';
+ }
+
+ /**
+ * Filter a menu Item Markup element's class attribute.
+ *
+ * @module custom-content-types
+ *
+ * @since 4.4.0
+ *
+ * @param string $tag Menu Item Markup element's class attribute.
+ * @param string $class Menu Item Class name.
+ * @param false|object $term Taxonomy term for current menu item.
+ */
+ return apply_filters(
+ 'jetpack_nova_menu_item_loop_class',
+ ' class="' . esc_attr( $class ) . '"',
+ $class,
+ $this->menu_item_loop_current_term
+ );
+ }
+}
+
+add_action( 'init', array( 'Nova_Restaurant', 'init' ) );
diff --git a/plugins/jetpack/modules/custom-post-types/portfolios.php b/plugins/jetpack/modules/custom-post-types/portfolios.php
new file mode 100644
index 00000000..dd3858ee
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/portfolios.php
@@ -0,0 +1,929 @@
+<?php
+
+class Jetpack_Portfolio {
+ const CUSTOM_POST_TYPE = 'jetpack-portfolio';
+ const CUSTOM_TAXONOMY_TYPE = 'jetpack-portfolio-type';
+ const CUSTOM_TAXONOMY_TAG = 'jetpack-portfolio-tag';
+ const OPTION_NAME = 'jetpack_portfolio';
+ const OPTION_READING_SETTING = 'jetpack_portfolio_posts_per_page';
+
+ public $version = '0.1';
+
+ static function init() {
+ static $instance = false;
+
+ if ( ! $instance ) {
+ $instance = new Jetpack_Portfolio;
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Conditionally hook into WordPress.
+ *
+ * Setup user option for enabling CPT
+ * If user has CPT enabled, show in admin
+ */
+ function __construct() {
+ // Add an option to enable the CPT
+ add_action( 'admin_init', array( $this, 'settings_api_init' ) );
+
+ // Check on theme switch if theme supports CPT and setting is disabled
+ add_action( 'after_switch_theme', array( $this, 'activation_post_type_support' ) );
+
+ // Make sure the post types are loaded for imports
+ add_action( 'import_start', array( $this, 'register_post_types' ) );
+
+ // Add to REST API post type whitelist
+ add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_portfolio_rest_api_type' ) );
+
+ $setting = Jetpack_Options::get_option_and_ensure_autoload( self::OPTION_NAME, '0' );
+
+ // Bail early if Portfolio option is not set and the theme doesn't declare support
+ if ( empty( $setting ) && ! $this->site_supports_custom_post_type() ) {
+ return;
+ }
+
+ // CPT magic
+ $this->register_post_types();
+ add_action( sprintf( 'add_option_%s', self::OPTION_NAME ), array( $this, 'flush_rules_on_enable' ), 10 );
+ add_action( sprintf( 'update_option_%s', self::OPTION_NAME ), array( $this, 'flush_rules_on_enable' ), 10 );
+ add_action( sprintf( 'publish_%s', self::CUSTOM_POST_TYPE), array( $this, 'flush_rules_on_first_project' ) );
+ add_action( 'after_switch_theme', array( $this, 'flush_rules_on_switch' ) );
+
+ // Admin Customization
+ add_filter( 'post_updated_messages', array( $this, 'updated_messages' ) );
+ add_filter( sprintf( 'manage_%s_posts_columns', self::CUSTOM_POST_TYPE), array( $this, 'edit_admin_columns' ) );
+ add_filter( sprintf( 'manage_%s_posts_custom_column', self::CUSTOM_POST_TYPE), array( $this, 'image_column' ), 10, 2 );
+ add_action( 'customize_register', array( $this, 'customize_register' ) );
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+
+ // Track all the things
+ add_action( sprintf( 'add_option_%s', self::OPTION_NAME ), array( $this, 'new_activation_stat_bump' ) );
+ add_action( sprintf( 'update_option_%s', self::OPTION_NAME ), array( $this, 'update_option_stat_bump' ), 11, 2 );
+ add_action( sprintf( 'publish_%s', self::CUSTOM_POST_TYPE), array( $this, 'new_project_stat_bump' ) );
+ }
+
+ add_image_size( 'jetpack-portfolio-admin-thumb', 50, 50, true );
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
+
+ // register jetpack_portfolio shortcode and portfolio shortcode (legacy)
+ add_shortcode( 'portfolio', array( $this, 'portfolio_shortcode' ) );
+ add_shortcode( 'jetpack_portfolio', array( $this, 'portfolio_shortcode' ) );
+
+ // Adjust CPT archive and custom taxonomies to obey CPT reading setting
+ add_filter( 'infinite_scroll_settings', array( $this, 'infinite_scroll_click_posts_per_page' ) );
+ add_filter( 'infinite_scroll_results', array( $this, 'infinite_scroll_results' ), 10, 3 );
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ // Add to Dotcom XML sitemaps
+ add_filter( 'wpcom_sitemap_post_types', array( $this, 'add_to_sitemap' ) );
+ } else {
+ // Add to Jetpack XML sitemap
+ add_filter( 'jetpack_sitemap_post_types', array( $this, 'add_to_sitemap' ) );
+ }
+
+ // Adjust CPT archive and custom taxonomies to obey CPT reading setting
+ add_filter( 'pre_get_posts', array( $this, 'query_reading_setting' ) );
+
+ // If CPT was enabled programatically and no CPT items exist when user switches away, disable
+ if ( $setting && $this->site_supports_custom_post_type() ) {
+ add_action( 'switch_theme', array( $this, 'deactivation_post_type_support' ) );
+ }
+ }
+
+ /**
+ * Add a checkbox field in 'Settings' > 'Writing'
+ * for enabling CPT functionality.
+ *
+ * @return null
+ */
+ function settings_api_init() {
+ add_settings_field(
+ self::OPTION_NAME,
+ '<span class="cpt-options">' . __( 'Portfolio Projects', 'jetpack' ) . '</span>',
+ array( $this, 'setting_html' ),
+ 'writing',
+ 'jetpack_cpt_section'
+ );
+ register_setting(
+ 'writing',
+ self::OPTION_NAME,
+ 'intval'
+ );
+
+ // Check if CPT is enabled first so that intval doesn't get set to NULL on re-registering
+ if ( get_option( self::OPTION_NAME, '0' ) || current_theme_supports( self::CUSTOM_POST_TYPE ) ) {
+ register_setting(
+ 'writing',
+ self::OPTION_READING_SETTING,
+ 'intval'
+ );
+ }
+ }
+
+ /**
+ * HTML code to display a checkbox true/false option
+ * for the Portfolio CPT setting.
+ *
+ * @return html
+ */
+ function setting_html() {
+ if ( current_theme_supports( self::CUSTOM_POST_TYPE ) ) : ?>
+ <p><?php printf( /* translators: %s is the name of a custom post type such as "jetpack-portfolio" */ __( 'Your theme supports <strong>%s</strong>', 'jetpack' ), self::CUSTOM_POST_TYPE ); ?></p>
+ <?php else : ?>
+ <label for="<?php echo esc_attr( self::OPTION_NAME ); ?>">
+ <input name="<?php echo esc_attr( self::OPTION_NAME ); ?>" id="<?php echo esc_attr( self::OPTION_NAME ); ?>" <?php echo checked( get_option( self::OPTION_NAME, '0' ), true, false ); ?> type="checkbox" value="1" />
+ <?php esc_html_e( 'Enable Portfolio Projects for this site.', 'jetpack' ); ?>
+ <a target="_blank" href="http://en.support.wordpress.com/portfolios/"><?php esc_html_e( 'Learn More', 'jetpack' ); ?></a>
+ </label>
+ <?php endif;
+ if ( get_option( self::OPTION_NAME, '0' ) || current_theme_supports( self::CUSTOM_POST_TYPE ) ) :
+ printf( '<p><label for="%1$s">%2$s</label></p>',
+ esc_attr( self::OPTION_READING_SETTING ),
+ /* translators: %1$s is replaced with an input field for numbers */
+ sprintf( __( 'Portfolio pages display at most %1$s projects', 'jetpack' ),
+ sprintf( '<input name="%1$s" id="%1$s" type="number" step="1" min="1" value="%2$s" class="small-text" />',
+ esc_attr( self::OPTION_READING_SETTING ),
+ esc_attr( get_option( self::OPTION_READING_SETTING, '10' ) )
+ )
+ )
+ );
+ endif;
+ }
+
+ /*
+ * Bump Portfolio > New Activation stat
+ */
+ function new_activation_stat_bump() {
+ bump_stats_extras( 'portfolios', 'new-activation' );
+ }
+
+ /*
+ * Bump Portfolio > Option On/Off stats to get total active
+ */
+ function update_option_stat_bump( $old, $new ) {
+ if ( empty( $old ) && ! empty( $new ) ) {
+ bump_stats_extras( 'portfolios', 'option-on' );
+ }
+
+ if ( ! empty( $old ) && empty( $new ) ) {
+ bump_stats_extras( 'portfolios', 'option-off' );
+ }
+ }
+
+ /*
+ * Bump Portfolio > Published Projects stat when projects are published
+ */
+ function new_project_stat_bump() {
+ bump_stats_extras( 'portfolios', 'published-projects' );
+ }
+
+ /**
+ * Should this Custom Post Type be made available?
+ */
+ function site_supports_custom_post_type() {
+ // If the current theme requests it.
+ if ( current_theme_supports( self::CUSTOM_POST_TYPE ) || get_option( self::OPTION_NAME, '0' ) ) {
+ return true;
+ }
+
+ // Otherwise, say no unless something wants to filter us to say yes.
+ /** This action is documented in modules/custom-post-types/nova.php */
+ return (bool) apply_filters( 'jetpack_enable_cpt', false, self::CUSTOM_POST_TYPE );
+ }
+
+ /*
+ * Flush permalinks when CPT option is turned on/off
+ */
+ function flush_rules_on_enable() {
+ flush_rewrite_rules();
+ }
+
+ /*
+ * Count published projects and flush permalinks when first projects is published
+ */
+ function flush_rules_on_first_project() {
+ $projects = get_transient( 'jetpack-portfolio-count-cache' );
+
+ if ( false === $projects ) {
+ flush_rewrite_rules();
+ $projects = (int) wp_count_posts( self::CUSTOM_POST_TYPE )->publish;
+
+ if ( ! empty( $projects ) ) {
+ set_transient( 'jetpack-portfolio-count-cache', $projects, HOUR_IN_SECONDS * 12 );
+ }
+ }
+ }
+
+ /*
+ * Flush permalinks when CPT supported theme is activated
+ */
+ function flush_rules_on_switch() {
+ if ( current_theme_supports( self::CUSTOM_POST_TYPE ) ) {
+ flush_rewrite_rules();
+ }
+ }
+
+ /**
+ * On plugin/theme activation, check if current theme supports CPT
+ */
+ static function activation_post_type_support() {
+ if ( current_theme_supports( self::CUSTOM_POST_TYPE ) ) {
+ update_option( self::OPTION_NAME, '1' );
+ }
+ }
+
+ /**
+ * On theme switch, check if CPT item exists and disable if not
+ */
+ function deactivation_post_type_support() {
+ $portfolios = get_posts( array(
+ 'fields' => 'ids',
+ 'posts_per_page' => 1,
+ 'post_type' => self::CUSTOM_POST_TYPE,
+ 'suppress_filters' => false
+ ) );
+
+ if ( empty( $portfolios ) ) {
+ update_option( self::OPTION_NAME, '0' );
+ }
+ }
+
+ /**
+ * Register Post Type
+ */
+ function register_post_types() {
+ if ( post_type_exists( self::CUSTOM_POST_TYPE ) ) {
+ return;
+ }
+
+ register_post_type( self::CUSTOM_POST_TYPE, array(
+ 'labels' => array(
+ 'name' => esc_html__( 'Projects', 'jetpack' ),
+ 'singular_name' => esc_html__( 'Project', 'jetpack' ),
+ 'menu_name' => esc_html__( 'Portfolio', 'jetpack' ),
+ 'all_items' => esc_html__( 'All Projects', 'jetpack' ),
+ 'add_new' => esc_html__( 'Add New', 'jetpack' ),
+ 'add_new_item' => esc_html__( 'Add New Project', 'jetpack' ),
+ 'edit_item' => esc_html__( 'Edit Project', 'jetpack' ),
+ 'new_item' => esc_html__( 'New Project', 'jetpack' ),
+ 'view_item' => esc_html__( 'View Project', 'jetpack' ),
+ 'search_items' => esc_html__( 'Search Projects', 'jetpack' ),
+ 'not_found' => esc_html__( 'No Projects found', 'jetpack' ),
+ 'not_found_in_trash' => esc_html__( 'No Projects found in Trash', 'jetpack' ),
+ 'filter_items_list' => esc_html__( 'Filter projects list', 'jetpack' ),
+ 'items_list_navigation' => esc_html__( 'Project list navigation', 'jetpack' ),
+ 'items_list' => esc_html__( 'Projects list', 'jetpack' ),
+ ),
+ 'supports' => array(
+ 'title',
+ 'editor',
+ 'thumbnail',
+ 'author',
+ 'comments',
+ 'publicize',
+ 'wpcom-markdown',
+ 'revisions',
+ 'excerpt',
+ ),
+ 'rewrite' => array(
+ 'slug' => 'portfolio',
+ 'with_front' => false,
+ 'feeds' => true,
+ 'pages' => true,
+ ),
+ 'public' => true,
+ 'show_ui' => true,
+ 'menu_position' => 20, // below Pages
+ 'menu_icon' => 'dashicons-portfolio', // 3.8+ dashicon option
+ 'capability_type' => 'page',
+ 'map_meta_cap' => true,
+ 'taxonomies' => array( self::CUSTOM_TAXONOMY_TYPE, self::CUSTOM_TAXONOMY_TAG ),
+ 'has_archive' => true,
+ 'query_var' => 'portfolio',
+ 'show_in_rest' => true,
+ ) );
+
+ register_taxonomy( self::CUSTOM_TAXONOMY_TYPE, self::CUSTOM_POST_TYPE, array(
+ 'hierarchical' => true,
+ 'labels' => array(
+ 'name' => esc_html__( 'Project Types', 'jetpack' ),
+ 'singular_name' => esc_html__( 'Project Type', 'jetpack' ),
+ 'menu_name' => esc_html__( 'Project Types', 'jetpack' ),
+ 'all_items' => esc_html__( 'All Project Types', 'jetpack' ),
+ 'edit_item' => esc_html__( 'Edit Project Type', 'jetpack' ),
+ 'view_item' => esc_html__( 'View Project Type', 'jetpack' ),
+ 'update_item' => esc_html__( 'Update Project Type', 'jetpack' ),
+ 'add_new_item' => esc_html__( 'Add New Project Type', 'jetpack' ),
+ 'new_item_name' => esc_html__( 'New Project Type Name', 'jetpack' ),
+ 'parent_item' => esc_html__( 'Parent Project Type', 'jetpack' ),
+ 'parent_item_colon' => esc_html__( 'Parent Project Type:', 'jetpack' ),
+ 'search_items' => esc_html__( 'Search Project Types', 'jetpack' ),
+ 'items_list_navigation' => esc_html__( 'Project type list navigation', 'jetpack' ),
+ 'items_list' => esc_html__( 'Project type list', 'jetpack' ),
+ ),
+ 'public' => true,
+ 'show_ui' => true,
+ 'show_in_nav_menus' => true,
+ 'show_in_rest' => true,
+ 'show_admin_column' => true,
+ 'query_var' => true,
+ 'rewrite' => array( 'slug' => 'project-type' ),
+ ) );
+
+ register_taxonomy( self::CUSTOM_TAXONOMY_TAG, self::CUSTOM_POST_TYPE, array(
+ 'hierarchical' => false,
+ 'labels' => array(
+ 'name' => esc_html__( 'Project Tags', 'jetpack' ),
+ 'singular_name' => esc_html__( 'Project Tag', 'jetpack' ),
+ 'menu_name' => esc_html__( 'Project Tags', 'jetpack' ),
+ 'all_items' => esc_html__( 'All Project Tags', 'jetpack' ),
+ 'edit_item' => esc_html__( 'Edit Project Tag', 'jetpack' ),
+ 'view_item' => esc_html__( 'View Project Tag', 'jetpack' ),
+ 'update_item' => esc_html__( 'Update Project Tag', 'jetpack' ),
+ 'add_new_item' => esc_html__( 'Add New Project Tag', 'jetpack' ),
+ 'new_item_name' => esc_html__( 'New Project Tag Name', 'jetpack' ),
+ 'search_items' => esc_html__( 'Search Project Tags', 'jetpack' ),
+ 'popular_items' => esc_html__( 'Popular Project Tags', 'jetpack' ),
+ 'separate_items_with_commas' => esc_html__( 'Separate tags with commas', 'jetpack' ),
+ 'add_or_remove_items' => esc_html__( 'Add or remove tags', 'jetpack' ),
+ 'choose_from_most_used' => esc_html__( 'Choose from the most used tags', 'jetpack' ),
+ 'not_found' => esc_html__( 'No tags found.', 'jetpack' ),
+ 'items_list_navigation' => esc_html__( 'Project tag list navigation', 'jetpack' ),
+ 'items_list' => esc_html__( 'Project tag list', 'jetpack' ),
+ ),
+ 'public' => true,
+ 'show_ui' => true,
+ 'show_in_nav_menus' => true,
+ 'show_in_rest' => true,
+ 'show_admin_column' => true,
+ 'query_var' => true,
+ 'rewrite' => array( 'slug' => 'project-tag' ),
+ ) );
+ }
+
+ /**
+ * Update messages for the Portfolio admin.
+ */
+ function updated_messages( $messages ) {
+ global $post;
+
+ $messages[self::CUSTOM_POST_TYPE] = array(
+ 0 => '', // Unused. Messages start at index 1.
+ 1 => sprintf( __( 'Project updated. <a href="%s">View item</a>', 'jetpack'), esc_url( get_permalink( $post->ID ) ) ),
+ 2 => esc_html__( 'Custom field updated.', 'jetpack' ),
+ 3 => esc_html__( 'Custom field deleted.', 'jetpack' ),
+ 4 => esc_html__( 'Project updated.', 'jetpack' ),
+ /* translators: %s: date and time of the revision */
+ 5 => isset( $_GET['revision'] ) ? sprintf( esc_html__( 'Project restored to revision from %s', 'jetpack'), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
+ 6 => sprintf( __( 'Project published. <a href="%s">View project</a>', 'jetpack' ), esc_url( get_permalink( $post->ID ) ) ),
+ 7 => esc_html__( 'Project saved.', 'jetpack' ),
+ 8 => sprintf( __( 'Project submitted. <a target="_blank" href="%s">Preview project</a>', 'jetpack'), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ),
+ 9 => sprintf( __( 'Project scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview project</a>', 'jetpack' ),
+ // translators: Publish box date format, see http://php.net/date
+ date_i18n( __( 'M j, Y @ G:i', 'jetpack' ), strtotime( $post->post_date ) ), esc_url( get_permalink( $post->ID ) ) ),
+ 10 => sprintf( __( 'Project item draft updated. <a target="_blank" href="%s">Preview project</a>', 'jetpack' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ),
+ );
+
+ return $messages;
+ }
+
+ /**
+ * Change ‘Title’ column label
+ * Add Featured Image column
+ */
+ function edit_admin_columns( $columns ) {
+ // change 'Title' to 'Project'
+ $columns['title'] = __( 'Project', 'jetpack' );
+ if ( current_theme_supports( 'post-thumbnails' ) ) {
+ // add featured image before 'Project'
+ $columns = array_slice( $columns, 0, 1, true ) + array( 'thumbnail' => '' ) + array_slice( $columns, 1, NULL, true );
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Add featured image to column
+ */
+ function image_column( $column, $post_id ) {
+ global $post;
+ switch ( $column ) {
+ case 'thumbnail':
+ echo get_the_post_thumbnail( $post_id, 'jetpack-portfolio-admin-thumb' );
+ break;
+ }
+ }
+
+ /**
+ * Adjust image column width
+ */
+ function enqueue_admin_styles( $hook ) {
+ $screen = get_current_screen();
+
+ if ( 'edit.php' == $hook && self::CUSTOM_POST_TYPE == $screen->post_type && current_theme_supports( 'post-thumbnails' ) ) {
+ wp_add_inline_style( 'wp-admin', '.manage-column.column-thumbnail { width: 50px; } @media screen and (max-width: 360px) { .column-thumbnail{ display:none; } }' );
+ }
+ }
+
+ /**
+ * Adds portfolio section to the Customizer.
+ */
+ function customize_register( $wp_customize ) {
+ $options = get_theme_support( self::CUSTOM_POST_TYPE );
+
+ if ( ( ! isset( $options[0]['title'] ) || true !== $options[0]['title'] ) && ( ! isset( $options[0]['content'] ) || true !== $options[0]['content'] ) && ( ! isset( $options[0]['featured-image'] ) || true !== $options[0]['featured-image'] ) ) {
+ return;
+ }
+
+ $wp_customize->add_section( 'jetpack_portfolio', array(
+ 'title' => esc_html__( 'Portfolio', 'jetpack' ),
+ 'theme_supports' => self::CUSTOM_POST_TYPE,
+ 'priority' => 130,
+ ) );
+
+ if ( isset( $options[0]['title'] ) && true === $options[0]['title'] ) {
+ $wp_customize->add_setting( 'jetpack_portfolio_title', array(
+ 'default' => esc_html__( 'Projects', 'jetpack' ),
+ 'type' => 'option',
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'sanitize_js_callback' => 'sanitize_text_field',
+ ) );
+
+ $wp_customize->add_control( 'jetpack_portfolio_title', array(
+ 'section' => 'jetpack_portfolio',
+ 'label' => esc_html__( 'Portfolio Archive Title', 'jetpack' ),
+ 'type' => 'text',
+ ) );
+ }
+
+ if ( isset( $options[0]['content'] ) && true === $options[0]['content'] ) {
+ $wp_customize->add_setting( 'jetpack_portfolio_content', array(
+ 'default' => '',
+ 'type' => 'option',
+ 'sanitize_callback' => 'wp_kses_post',
+ 'sanitize_js_callback' => 'wp_kses_post',
+ ) );
+
+ $wp_customize->add_control( 'jetpack_portfolio_content', array(
+ 'section' => 'jetpack_portfolio',
+ 'label' => esc_html__( 'Portfolio Archive Content', 'jetpack' ),
+ 'type' => 'textarea',
+ ) );
+ }
+
+ if ( isset( $options[0]['featured-image'] ) && true === $options[0]['featured-image'] ) {
+ $wp_customize->add_setting( 'jetpack_portfolio_featured_image', array(
+ 'default' => '',
+ 'type' => 'option',
+ 'sanitize_callback' => 'attachment_url_to_postid',
+ 'sanitize_js_callback' => 'attachment_url_to_postid',
+ 'theme_supports' => 'post-thumbnails',
+ ) );
+
+ $wp_customize->add_control( new WP_Customize_Image_Control( $wp_customize, 'jetpack_portfolio_featured_image', array(
+ 'section' => 'jetpack_portfolio',
+ 'label' => esc_html__( 'Portfolio Archive Featured Image', 'jetpack' ),
+ ) ) );
+ }
+ }
+
+ /**
+ * Follow CPT reading setting on CPT archive and taxonomy pages
+ */
+ function query_reading_setting( $query ) {
+ if ( ( ! is_admin() || ( is_admin() && defined( 'DOING_AJAX' ) && DOING_AJAX ) )
+ && $query->is_main_query()
+ && ( $query->is_post_type_archive( self::CUSTOM_POST_TYPE )
+ || $query->is_tax( self::CUSTOM_TAXONOMY_TYPE )
+ || $query->is_tax( self::CUSTOM_TAXONOMY_TAG ) )
+ ) {
+ $query->set( 'posts_per_page', get_option( self::OPTION_READING_SETTING, '10' ) );
+ }
+ }
+
+ /*
+ * If Infinite Scroll is set to 'click', use our custom reading setting instead of core's `posts_per_page`.
+ */
+ function infinite_scroll_click_posts_per_page( $settings ) {
+ global $wp_query;
+
+ if ( ( ! is_admin() || ( is_admin() && defined( 'DOING_AJAX' ) && DOING_AJAX ) )
+ && true === $settings['click_handle']
+ && ( $wp_query->is_post_type_archive( self::CUSTOM_POST_TYPE )
+ || $wp_query->is_tax( self::CUSTOM_TAXONOMY_TYPE )
+ || $wp_query->is_tax( self::CUSTOM_TAXONOMY_TAG ) )
+ ) {
+ $settings['posts_per_page'] = get_option( self::OPTION_READING_SETTING, $settings['posts_per_page'] );
+ }
+
+ return $settings;
+ }
+
+ /*
+ * Filter the results of infinite scroll to make sure we get `lastbatch` right.
+ */
+ function infinite_scroll_results( $results, $query_args, $query ) {
+ $results['lastbatch'] = $query_args['paged'] >= $query->max_num_pages;
+ return $results;
+ }
+
+ /**
+ * Add CPT to Dotcom sitemap
+ */
+ function add_to_sitemap( $post_types ) {
+ $post_types[] = self::CUSTOM_POST_TYPE;
+
+ return $post_types;
+ }
+
+ /**
+ * Add to REST API post type whitelist
+ */
+ function allow_portfolio_rest_api_type( $post_types ) {
+ $post_types[] = self::CUSTOM_POST_TYPE;
+
+ return $post_types;
+ }
+
+ /**
+ * Our [portfolio] shortcode.
+ * Prints Portfolio data styled to look good on *any* theme.
+ *
+ * @return portfolio_shortcode_html
+ */
+ static function portfolio_shortcode( $atts ) {
+ // Default attributes
+ $atts = shortcode_atts( array(
+ 'display_types' => true,
+ 'display_tags' => true,
+ 'display_content' => true,
+ 'display_author' => false,
+ 'show_filter' => false,
+ 'include_type' => false,
+ 'include_tag' => false,
+ 'columns' => 2,
+ 'showposts' => -1,
+ 'order' => 'asc',
+ 'orderby' => 'date',
+ ), $atts, 'portfolio' );
+
+ // A little sanitization
+ if ( $atts['display_types'] && 'true' != $atts['display_types'] ) {
+ $atts['display_types'] = false;
+ }
+
+ if ( $atts['display_tags'] && 'true' != $atts['display_tags'] ) {
+ $atts['display_tags'] = false;
+ }
+
+ if ( $atts['display_author'] && 'true' != $atts['display_author'] ) {
+ $atts['display_author'] = false;
+ }
+
+ if ( $atts['display_content'] && 'true' != $atts['display_content'] && 'full' != $atts['display_content'] ) {
+ $atts['display_content'] = false;
+ }
+
+ if ( $atts['include_type'] ) {
+ $atts['include_type'] = explode( ',', str_replace( ' ', '', $atts['include_type'] ) );
+ }
+
+ if ( $atts['include_tag'] ) {
+ $atts['include_tag'] = explode( ',', str_replace( ' ', '', $atts['include_tag'] ) );
+ }
+
+ $atts['columns'] = absint( $atts['columns'] );
+
+ $atts['showposts'] = intval( $atts['showposts'] );
+
+
+ if ( $atts['order'] ) {
+ $atts['order'] = urldecode( $atts['order'] );
+ $atts['order'] = strtoupper( $atts['order'] );
+ if ( 'DESC' != $atts['order'] ) {
+ $atts['order'] = 'ASC';
+ }
+ }
+
+ if ( $atts['orderby'] ) {
+ $atts['orderby'] = urldecode( $atts['orderby'] );
+ $atts['orderby'] = strtolower( $atts['orderby'] );
+ $allowed_keys = array( 'author', 'date', 'title', 'rand' );
+
+ $parsed = array();
+ foreach ( explode( ',', $atts['orderby'] ) as $portfolio_index_number => $orderby ) {
+ if ( ! in_array( $orderby, $allowed_keys ) ) {
+ continue;
+ }
+ $parsed[] = $orderby;
+ }
+
+ if ( empty( $parsed ) ) {
+ unset( $atts['orderby'] );
+ } else {
+ $atts['orderby'] = implode( ' ', $parsed );
+ }
+ }
+
+ // enqueue shortcode styles when shortcode is used
+ wp_enqueue_style( 'jetpack-portfolio-style', plugins_url( 'css/portfolio-shortcode.css', __FILE__ ), array(), '20140326' );
+
+ return self::portfolio_shortcode_html( $atts );
+ }
+
+ /**
+ * Query to retrieve entries from the Portfolio post_type.
+ *
+ * @return object
+ */
+ static function portfolio_query( $atts ) {
+ // Default query arguments
+ $default = array(
+ 'order' => $atts['order'],
+ 'orderby' => $atts['orderby'],
+ 'posts_per_page' => $atts['showposts'],
+ );
+
+ $args = wp_parse_args( $atts, $default );
+ $args['post_type'] = self::CUSTOM_POST_TYPE; // Force this post type
+
+ if ( false != $atts['include_type'] || false != $atts['include_tag'] ) {
+ $args['tax_query'] = array();
+ }
+
+ // If 'include_type' has been set use it on the main query
+ if ( false != $atts['include_type'] ) {
+ array_push( $args['tax_query'], array(
+ 'taxonomy' => self::CUSTOM_TAXONOMY_TYPE,
+ 'field' => 'slug',
+ 'terms' => $atts['include_type'],
+ ) );
+ }
+
+ // If 'include_tag' has been set use it on the main query
+ if ( false != $atts['include_tag'] ) {
+ array_push( $args['tax_query'], array(
+ 'taxonomy' => self::CUSTOM_TAXONOMY_TAG,
+ 'field' => 'slug',
+ 'terms' => $atts['include_tag'],
+ ) );
+ }
+
+ if ( false != $atts['include_type'] && false != $atts['include_tag'] ) {
+ $args['tax_query']['relation'] = 'AND';
+ }
+
+ // Run the query and return
+ $query = new WP_Query( $args );
+ return $query;
+ }
+
+ /**
+ * The Portfolio shortcode loop.
+ *
+ * @todo add theme color styles
+ * @return html
+ */
+ static function portfolio_shortcode_html( $atts ) {
+
+ $query = self::portfolio_query( $atts );
+ $portfolio_index_number = 0;
+
+ ob_start();
+
+ // If we have posts, create the html
+ // with hportfolio markup
+ if ( $query->have_posts() ) {
+
+ // Render styles
+ //self::themecolor_styles();
+
+ ?>
+ <div class="jetpack-portfolio-shortcode column-<?php echo esc_attr( $atts['columns'] ); ?>">
+ <?php // open .jetpack-portfolio
+
+ // Construct the loop...
+ while ( $query->have_posts() ) {
+ $query->the_post();
+ $post_id = get_the_ID();
+ ?>
+ <div class="portfolio-entry <?php echo esc_attr( self::get_project_class( $portfolio_index_number, $atts['columns'] ) ); ?>">
+ <header class="portfolio-entry-header">
+ <?php
+ // Featured image
+ echo self::get_portfolio_thumbnail_link( $post_id );
+ ?>
+
+ <h2 class="portfolio-entry-title"><a href="<?php echo esc_url( get_permalink() ); ?>" title="<?php echo esc_attr( the_title_attribute( ) ); ?>"><?php the_title(); ?></a></h2>
+
+ <div class="portfolio-entry-meta">
+ <?php
+ if ( false != $atts['display_types'] ) {
+ echo self::get_project_type( $post_id );
+ }
+
+ if ( false != $atts['display_tags'] ) {
+ echo self::get_project_tags( $post_id );
+ }
+
+ if ( false != $atts['display_author'] ) {
+ echo self::get_project_author( $post_id );
+ }
+ ?>
+ </div>
+
+ </header>
+
+ <?php
+ // The content
+ if ( false !== $atts['display_content'] ) {
+ add_filter( 'wordads_inpost_disable', '__return_true', 20 );
+ if ( 'full' === $atts['display_content'] ) {
+ ?>
+ <div class="portfolio-entry-content"><?php the_content(); ?></div>
+ <?php
+ } else {
+ ?>
+ <div class="portfolio-entry-content"><?php the_excerpt(); ?></div>
+ <?php
+ }
+ remove_filter( 'wordads_inpost_disable', '__return_true', 20 );
+ }
+ ?>
+ </div><!-- close .portfolio-entry -->
+ <?php $portfolio_index_number++;
+ } // end of while loop
+
+ wp_reset_postdata();
+ ?>
+ </div><!-- close .jetpack-portfolio -->
+ <?php
+ } else { ?>
+ <p><em><?php _e( 'Your Portfolio Archive currently has no entries. You can start creating them on your dashboard.', 'jetpack' ); ?></p></em>
+ <?php
+ }
+ $html = ob_get_clean();
+
+ // If there is a [portfolio] within a [portfolio], remove the shortcode
+ if ( has_shortcode( $html, 'portfolio' ) ){
+ remove_shortcode( 'portfolio' );
+ }
+
+ // Return the HTML block
+ return $html;
+ }
+
+ /**
+ * Individual project class
+ *
+ * @return string
+ */
+ static function get_project_class( $portfolio_index_number, $columns ) {
+ $project_types = wp_get_object_terms( get_the_ID(), self::CUSTOM_TAXONOMY_TYPE, array( 'fields' => 'slugs' ) );
+ $class = array();
+
+ $class[] = 'portfolio-entry-column-'.$columns;
+ // add a type- class for each project type
+ foreach ( $project_types as $project_type ) {
+ $class[] = 'type-' . esc_html( $project_type );
+ }
+ if( $columns > 1) {
+ if ( ( $portfolio_index_number % 2 ) == 0 ) {
+ $class[] = 'portfolio-entry-mobile-first-item-row';
+ } else {
+ $class[] = 'portfolio-entry-mobile-last-item-row';
+ }
+ }
+
+ // add first and last classes to first and last items in a row
+ if ( ( $portfolio_index_number % $columns ) == 0 ) {
+ $class[] = 'portfolio-entry-first-item-row';
+ } elseif ( ( $portfolio_index_number % $columns ) == ( $columns - 1 ) ) {
+ $class[] = 'portfolio-entry-last-item-row';
+ }
+
+
+ /**
+ * Filter the class applied to project div in the portfolio
+ *
+ * @module custom-content-types
+ *
+ * @since 3.1.0
+ *
+ * @param string $class class name of the div.
+ * @param int $portfolio_index_number iterator count the number of columns up starting from 0.
+ * @param int $columns number of columns to display the content in.
+ *
+ */
+ return apply_filters( 'portfolio-project-post-class', implode( " ", $class ) , $portfolio_index_number, $columns );
+ }
+
+ /**
+ * Displays the project type that a project belongs to.
+ *
+ * @return html
+ */
+ static function get_project_type( $post_id ) {
+ $project_types = get_the_terms( $post_id, self::CUSTOM_TAXONOMY_TYPE );
+
+ // If no types, return empty string
+ if ( empty( $project_types ) || is_wp_error( $project_types ) ) {
+ return;
+ }
+
+ $html = '<div class="project-types"><span>' . __( 'Types', 'jetpack' ) . ':</span>';
+ $types = array();
+ // Loop thorugh all the types
+ foreach ( $project_types as $project_type ) {
+ $project_type_link = get_term_link( $project_type, self::CUSTOM_TAXONOMY_TYPE );
+
+ if ( is_wp_error( $project_type_link ) ) {
+ return $project_type_link;
+ }
+
+ $types[] = '<a href="' . esc_url( $project_type_link ) . '" rel="tag">' . esc_html( $project_type->name ) . '</a>';
+ }
+ $html .= ' '.implode( ', ', $types );
+ $html .= '</div>';
+
+ return $html;
+ }
+
+ /**
+ * Displays the project tags that a project belongs to.
+ *
+ * @return html
+ */
+ static function get_project_tags( $post_id ) {
+ $project_tags = get_the_terms( $post_id, self::CUSTOM_TAXONOMY_TAG );
+
+ // If no tags, return empty string
+ if ( empty( $project_tags ) || is_wp_error( $project_tags ) ) {
+ return false;
+ }
+
+ $html = '<div class="project-tags"><span>' . __( 'Tags', 'jetpack' ) . ':</span>';
+ $tags = array();
+ // Loop thorugh all the tags
+ foreach ( $project_tags as $project_tag ) {
+ $project_tag_link = get_term_link( $project_tag, self::CUSTOM_TAXONOMY_TYPE );
+
+ if ( is_wp_error( $project_tag_link ) ) {
+ return $project_tag_link;
+ }
+
+ $tags[] = '<a href="' . esc_url( $project_tag_link ) . '" rel="tag">' . esc_html( $project_tag->name ) . '</a>';
+ }
+ $html .= ' '. implode( ', ', $tags );
+ $html .= '</div>';
+
+ return $html;
+ }
+
+ /**
+ * Displays the author of the current portfolio project.
+ *
+ * @return html
+ */
+ static function get_project_author() {
+ $html = '<div class="project-author">';
+ /* translators: %1$s is link to author posts, %2$s is author display name */
+ $html .= sprintf( __( '<span>Author:</span> <a href="%1$s">%2$s</a>', 'jetpack' ),
+ esc_url( get_author_posts_url( get_the_author_meta( 'ID' ) ) ),
+ esc_html( get_the_author() )
+ );
+ $html .= '</div>';
+
+ return $html;
+ }
+
+ /**
+ * Display the featured image if it's available
+ *
+ * @return html
+ */
+ static function get_portfolio_thumbnail_link( $post_id ) {
+ if ( has_post_thumbnail( $post_id ) ) {
+ /**
+ * Change the Portfolio thumbnail size.
+ *
+ * @module custom-content-types
+ *
+ * @since 3.4.0
+ *
+ * @param string|array $var Either a registered size keyword or size array.
+ */
+ return '<a class="portfolio-featured-image" href="' . esc_url( get_permalink( $post_id ) ) . '">' . get_the_post_thumbnail( $post_id, apply_filters( 'jetpack_portfolio_thumbnail_size', 'large' ) ) . '</a>';
+ }
+ }
+}
+
+add_action( 'init', array( 'Jetpack_Portfolio', 'init' ) );
+
+// Check on plugin activation if theme supports CPT
+register_activation_hook( __FILE__, array( 'Jetpack_Portfolio', 'activation_post_type_support' ) );
+add_action( 'jetpack_activate_module_custom-content-types', array( 'Jetpack_Portfolio', 'activation_post_type_support' ) );
diff --git a/plugins/jetpack/modules/custom-post-types/testimonial.php b/plugins/jetpack/modules/custom-post-types/testimonial.php
new file mode 100644
index 00000000..ba427244
--- /dev/null
+++ b/plugins/jetpack/modules/custom-post-types/testimonial.php
@@ -0,0 +1,763 @@
+<?php
+
+class Jetpack_Testimonial {
+ const CUSTOM_POST_TYPE = 'jetpack-testimonial';
+ const OPTION_NAME = 'jetpack_testimonial';
+ const OPTION_READING_SETTING = 'jetpack_testimonial_posts_per_page';
+
+ public $version = '0.1';
+
+ static function init() {
+ static $instance = false;
+
+ if ( ! $instance ) {
+ $instance = new Jetpack_Testimonial;
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Conditionally hook into WordPress.
+ *
+ * Setup user option for enabling CPT.
+ * If user has CPT enabled, show in admin.
+ */
+ function __construct() {
+ // Make sure the post types are loaded for imports
+ add_action( 'import_start', array( $this, 'register_post_types' ) );
+
+ // If called via REST API, we need to register later in lifecycle
+ add_action( 'restapi_theme_init', array( $this, 'maybe_register_cpt' ) );
+
+ // Add to REST API post type whitelist
+ add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_cpt_rest_api_type' ) );
+
+ $this->maybe_register_cpt();
+ }
+
+ /**
+ * Registers the custom post types and adds action/filter handlers, but
+ * only if the site supports it
+ */
+ function maybe_register_cpt() {
+ // Add an option to enable the CPT
+ add_action( 'admin_init', array( $this, 'settings_api_init' ) );
+
+ // Check on theme switch if theme supports CPT and setting is disabled
+ add_action( 'after_switch_theme', array( $this, 'activation_post_type_support' ) );
+
+ $setting = Jetpack_Options::get_option_and_ensure_autoload( self::OPTION_NAME, '0' );
+
+ // Bail early if Testimonial option is not set and the theme doesn't declare support
+ if ( empty( $setting ) && ! $this->site_supports_custom_post_type() ) {
+ return;
+ }
+
+ if ( ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) && ! Jetpack::is_module_active( 'custom-content-types' ) ) {
+ return;
+ }
+
+ // CPT magic
+ $this->register_post_types();
+ add_action( sprintf( 'add_option_%s', self::OPTION_NAME ), array( $this, 'flush_rules_on_enable' ), 10 );
+ add_action( sprintf( 'update_option_%s', self::OPTION_NAME ), array( $this, 'flush_rules_on_enable' ), 10 );
+ add_action( sprintf( 'publish_%s', self::CUSTOM_POST_TYPE ), array( $this, 'flush_rules_on_first_testimonial' ) );
+ add_action( 'after_switch_theme', array( $this, 'flush_rules_on_switch' ) );
+
+ // Admin Customization
+ add_filter( 'enter_title_here', array( $this, 'change_default_title' ) );
+ add_filter( sprintf( 'manage_%s_posts_columns', self::CUSTOM_POST_TYPE), array( $this, 'edit_title_column_label' ) );
+ add_filter( 'post_updated_messages', array( $this, 'updated_messages' ) );
+ add_action( 'customize_register', array( $this, 'customize_register' ) );
+
+ // Only add the 'Customize' sub-menu if the theme supports it.
+ $num_testimonials = self::count_testimonials();
+ if ( ! empty( $num_testimonials ) && current_theme_supports( self::CUSTOM_POST_TYPE ) ) {
+ add_action( 'admin_menu', array( $this, 'add_customize_page' ) );
+ }
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ // Track all the things
+ add_action( sprintf( 'add_option_%s', self::OPTION_NAME ), array( $this, 'new_activation_stat_bump' ) );
+ add_action( sprintf( 'update_option_%s', self::OPTION_NAME ), array( $this, 'update_option_stat_bump' ), 11, 2 );
+ add_action( sprintf( 'publish_%s', self::CUSTOM_POST_TYPE), array( $this, 'new_testimonial_stat_bump' ) );
+
+ // Add to Dotcom XML sitemaps
+ add_filter( 'wpcom_sitemap_post_types', array( $this, 'add_to_sitemap' ) );
+ } else {
+ // Add to Jetpack XML sitemap
+ add_filter( 'jetpack_sitemap_post_types', array( $this, 'add_to_sitemap' ) );
+ }
+
+ // Adjust CPT archive and custom taxonomies to obey CPT reading setting
+ add_filter( 'pre_get_posts', array( $this, 'query_reading_setting' ), 20 );
+ add_filter( 'infinite_scroll_settings', array( $this, 'infinite_scroll_click_posts_per_page' ) );
+
+ // Register [jetpack_testimonials] always and
+ // register [testimonials] if [testimonials] isn't already set
+ add_shortcode( 'jetpack_testimonials', array( $this, 'jetpack_testimonial_shortcode' ) );
+
+ if ( ! shortcode_exists( 'testimonials' ) ) {
+ add_shortcode( 'testimonials', array( $this, 'jetpack_testimonial_shortcode' ) );
+ }
+
+ // If CPT was enabled programatically and no CPT items exist when user switches away, disable
+ if ( $setting && $this->site_supports_custom_post_type() ) {
+ add_action( 'switch_theme', array( $this, 'deactivation_post_type_support' ) );
+ }
+ }
+
+ /**
+ * Add a checkbox field in 'Settings' > 'Writing'
+ * for enabling CPT functionality.
+ *
+ * @return null
+ */
+ function settings_api_init() {
+ add_settings_field(
+ self::OPTION_NAME,
+ '<span class="cpt-options">' . __( 'Testimonials', 'jetpack' ) . '</span>',
+ array( $this, 'setting_html' ),
+ 'writing',
+ 'jetpack_cpt_section'
+ );
+
+ register_setting(
+ 'writing',
+ self::OPTION_NAME,
+ 'intval'
+ );
+
+ // Check if CPT is enabled first so that intval doesn't get set to NULL on re-registering
+ if ( $this->site_supports_custom_post_type() ) {
+ register_setting(
+ 'writing',
+ self::OPTION_READING_SETTING,
+ 'intval'
+ );
+ }
+ }
+
+ /**
+ * HTML code to display a checkbox true/false option
+ * for the CPT setting.
+ *
+ * @return html
+ */
+ function setting_html() {
+ if ( current_theme_supports( self::CUSTOM_POST_TYPE ) ) : ?>
+ <p><?php printf( __( 'Your theme supports Testimonials', 'jetpack' ) ); ?></p>
+ <?php else : ?>
+ <label for="<?php echo esc_attr( self::OPTION_NAME ); ?>">
+ <input name="<?php echo esc_attr( self::OPTION_NAME ); ?>" id="<?php echo esc_attr( self::OPTION_NAME ); ?>" <?php echo checked( get_option( self::OPTION_NAME, '0' ), true, false ); ?> type="checkbox" value="1" />
+ <?php esc_html_e( 'Enable Testimonials for this site.', 'jetpack' ); ?>
+ <a target="_blank" href="http://en.support.wordpress.com/testimonials/"><?php esc_html_e( 'Learn More', 'jetpack' ); ?></a>
+ </label>
+ <?php endif;
+
+ if ( $this->site_supports_custom_post_type() ) :
+ printf( '<p><label for="%1$s">%2$s</label></p>',
+ esc_attr( self::OPTION_READING_SETTING ),
+ /* translators: %1$s is replaced with an input field for numbers */
+ sprintf( __( 'Testimonial pages display at most %1$s testimonials', 'jetpack' ),
+ sprintf( '<input name="%1$s" id="%1$s" type="number" step="1" min="1" value="%2$s" class="small-text" />',
+ esc_attr( self::OPTION_READING_SETTING ),
+ esc_attr( get_option( self::OPTION_READING_SETTING, '10' ) )
+ )
+ )
+ );
+ endif;
+ }
+
+ /**
+ * Should this Custom Post Type be made available?
+ */
+ function site_supports_custom_post_type() {
+ // If the current theme requests it.
+ if ( current_theme_supports( self::CUSTOM_POST_TYPE ) || get_option( self::OPTION_NAME, '0' ) ) {
+ return true;
+ }
+
+ // Otherwise, say no unless something wants to filter us to say yes.
+ /** This action is documented in modules/custom-post-types/nova.php */
+ return (bool) apply_filters( 'jetpack_enable_cpt', false, self::CUSTOM_POST_TYPE );
+ }
+
+ /**
+ * Add to REST API post type whitelist
+ */
+ function allow_cpt_rest_api_type( $post_types ) {
+ $post_types[] = self::CUSTOM_POST_TYPE;
+
+ return $post_types;
+ }
+
+ /**
+ * Bump Testimonial > New Activation stat
+ */
+ function new_activation_stat_bump() {
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extras', 'testimonials', 'new-activation' );
+ }
+
+ /**
+ * Bump Testimonial > Option On/Off stats to get total active
+ */
+ function update_option_stat_bump( $old, $new ) {
+ if ( empty( $old ) && ! empty( $new ) ) {
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extras', 'testimonials', 'option-on' );
+ }
+
+ if ( ! empty( $old ) && empty( $new ) ) {
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extras', 'testimonials', 'option-off' );
+ }
+ }
+
+ /**
+ * Bump Testimonial > Published Testimonials stat when testimonials are published
+ */
+ function new_testimonial_stat_bump() {
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action ( 'jetpack_bump_stats_extras', 'testimonials', 'published-testimonials' );
+ }
+
+ /*
+ * Flush permalinks when CPT option is turned on/off
+ */
+ function flush_rules_on_enable() {
+ flush_rewrite_rules();
+ }
+
+ /*
+ * Count published testimonials and flush permalinks when first testimonial is published
+ */
+ function flush_rules_on_first_testimonial() {
+ $testimonials = get_transient( 'jetpack-testimonial-count-cache' );
+
+ if ( false === $testimonials ) {
+ flush_rewrite_rules();
+ $testimonials = (int) wp_count_posts( self::CUSTOM_POST_TYPE )->publish;
+
+ if ( ! empty( $testimonials ) ) {
+ set_transient( 'jetpack-testimonial-count-cache', $testimonials, HOUR_IN_SECONDS * 12 );
+ }
+ }
+ }
+
+ /*
+ * Flush permalinks when CPT supported theme is activated
+ */
+ function flush_rules_on_switch() {
+ if ( current_theme_supports( self::CUSTOM_POST_TYPE ) ) {
+ flush_rewrite_rules();
+ }
+ }
+
+ /**
+ * On plugin/theme activation, check if current theme supports CPT
+ */
+ static function activation_post_type_support() {
+ if ( current_theme_supports( self::CUSTOM_POST_TYPE ) ) {
+ update_option( self::OPTION_NAME, '1' );
+ }
+ }
+
+ /**
+ * On theme switch, check if CPT item exists and disable if not
+ */
+ function deactivation_post_type_support() {
+ $portfolios = get_posts( array(
+ 'fields' => 'ids',
+ 'posts_per_page' => 1,
+ 'post_type' => self::CUSTOM_POST_TYPE,
+ 'suppress_filters' => false
+ ) );
+
+ if ( empty( $portfolios ) ) {
+ update_option( self::OPTION_NAME, '0' );
+ }
+ }
+
+ /**
+ * Register Post Type
+ */
+ function register_post_types() {
+ if ( post_type_exists( self::CUSTOM_POST_TYPE ) ) {
+ return;
+ }
+
+ register_post_type( self::CUSTOM_POST_TYPE, array(
+ 'description' => __( 'Customer Testimonials', 'jetpack' ),
+ 'labels' => array(
+ 'name' => esc_html__( 'Testimonials', 'jetpack' ),
+ 'singular_name' => esc_html__( 'Testimonial', 'jetpack' ),
+ 'menu_name' => esc_html__( 'Testimonials', 'jetpack' ),
+ 'all_items' => esc_html__( 'All Testimonials', 'jetpack' ),
+ 'add_new' => esc_html__( 'Add New', 'jetpack' ),
+ 'add_new_item' => esc_html__( 'Add New Testimonial', 'jetpack' ),
+ 'edit_item' => esc_html__( 'Edit Testimonial', 'jetpack' ),
+ 'new_item' => esc_html__( 'New Testimonial', 'jetpack' ),
+ 'view_item' => esc_html__( 'View Testimonial', 'jetpack' ),
+ 'search_items' => esc_html__( 'Search Testimonials', 'jetpack' ),
+ 'not_found' => esc_html__( 'No Testimonials found', 'jetpack' ),
+ 'not_found_in_trash' => esc_html__( 'No Testimonials found in Trash', 'jetpack' ),
+ 'filter_items_list' => esc_html__( 'Filter Testimonials list', 'jetpack' ),
+ 'items_list_navigation' => esc_html__( 'Testimonial list navigation', 'jetpack' ),
+ 'items_list' => esc_html__( 'Testimonials list', 'jetpack' ),
+ ),
+ 'supports' => array(
+ 'title',
+ 'editor',
+ 'thumbnail',
+ 'page-attributes',
+ 'revisions',
+ 'excerpt',
+ ),
+ 'rewrite' => array(
+ 'slug' => 'testimonial',
+ 'with_front' => false,
+ 'feeds' => false,
+ 'pages' => true,
+ ),
+ 'public' => true,
+ 'show_ui' => true,
+ 'menu_position' => 20, // below Pages
+ 'menu_icon' => 'dashicons-testimonial',
+ 'capability_type' => 'page',
+ 'map_meta_cap' => true,
+ 'has_archive' => true,
+ 'query_var' => 'testimonial',
+ 'show_in_rest' => true,
+ ) );
+ }
+
+ /**
+ * Update messages for the Testimonial admin.
+ */
+ function updated_messages( $messages ) {
+ global $post;
+
+ $messages[ self::CUSTOM_POST_TYPE ] = array(
+ 0 => '', // Unused. Messages start at index 1.
+ 1 => sprintf( __( 'Testimonial updated. <a href="%s">View testimonial</a>', 'jetpack'), esc_url( get_permalink( $post->ID ) ) ),
+ 2 => esc_html__( 'Custom field updated.', 'jetpack' ),
+ 3 => esc_html__( 'Custom field deleted.', 'jetpack' ),
+ 4 => esc_html__( 'Testimonial updated.', 'jetpack' ),
+ /* translators: %s: date and time of the revision */
+ 5 => isset( $_GET['revision'] ) ? sprintf( esc_html__( 'Testimonial restored to revision from %s', 'jetpack'), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
+ 6 => sprintf( __( 'Testimonial published. <a href="%s">View testimonial</a>', 'jetpack' ), esc_url( get_permalink( $post->ID ) ) ),
+ 7 => esc_html__( 'Testimonial saved.', 'jetpack' ),
+ 8 => sprintf( __( 'Testimonial submitted. <a target="_blank" href="%s">Preview testimonial</a>', 'jetpack'), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ),
+ 9 => sprintf( __( 'Testimonial scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview testimonial</a>', 'jetpack' ),
+ // translators: Publish box date format, see http://php.net/date
+ date_i18n( __( 'M j, Y @ G:i', 'jetpack' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post->ID) ) ),
+ 10 => sprintf( __( 'Testimonial draft updated. <a target="_blank" href="%s">Preview testimonial</a>', 'jetpack' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ),
+ );
+
+ return $messages;
+ }
+
+ /**
+ * Change ‘Enter Title Here’ text for the Testimonial.
+ */
+ function change_default_title( $title ) {
+ if ( self::CUSTOM_POST_TYPE == get_post_type() ) {
+ $title = esc_html__( "Enter the customer's name here", 'jetpack' );
+ }
+
+ return $title;
+ }
+
+ /**
+ * Change ‘Title’ column label on all Testimonials page.
+ */
+ function edit_title_column_label( $columns ) {
+ $columns['title'] = esc_html__( 'Customer Name', 'jetpack' );
+
+ return $columns;
+ }
+
+ /**
+ * Follow CPT reading setting on CPT archive page
+ */
+ function query_reading_setting( $query ) {
+ if ( ! is_admin()
+ && $query->is_main_query()
+ && $query->is_post_type_archive( self::CUSTOM_POST_TYPE )
+ ) {
+ $query->set( 'posts_per_page', get_option( self::OPTION_READING_SETTING, '10' ) );
+ }
+ }
+
+ /*
+ * If Infinite Scroll is set to 'click', use our custom reading setting instead of core's `posts_per_page`.
+ */
+ function infinite_scroll_click_posts_per_page( $settings ) {
+ global $wp_query;
+
+ if ( ! is_admin() && true === $settings['click_handle'] && $wp_query->is_post_type_archive( self::CUSTOM_POST_TYPE ) ) {
+ $settings['posts_per_page'] = get_option( self::OPTION_READING_SETTING, $settings['posts_per_page'] );
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Add CPT to Dotcom sitemap
+ */
+ function add_to_sitemap( $post_types ) {
+ $post_types[] = self::CUSTOM_POST_TYPE;
+
+ return $post_types;
+ }
+
+ function set_testimonial_option() {
+ $testimonials = wp_count_posts( self::CUSTOM_POST_TYPE );
+ $published_testimonials = $testimonials->publish;
+
+ update_option( self::OPTION_NAME, $published_testimonials );
+ }
+
+ function count_testimonials() {
+ $testimonials = get_transient( 'jetpack-testimonial-count-cache' );
+
+ if ( false === $testimonials ) {
+ $testimonials = (int) wp_count_posts( self::CUSTOM_POST_TYPE )->publish;
+
+ if ( ! empty( $testimonials ) ) {
+ set_transient( 'jetpack-testimonial-count-cache', $testimonials, 60*60*12 );
+ }
+ }
+
+ return $testimonials;
+ }
+
+ /**
+ * Adds a submenu link to the Customizer.
+ */
+ function add_customize_page() {
+ add_submenu_page(
+ 'edit.php?post_type=' . self::CUSTOM_POST_TYPE,
+ esc_html__( 'Customize Testimonials Archive', 'jetpack' ),
+ esc_html__( 'Customize', 'jetpack' ),
+ 'edit_theme_options',
+ add_query_arg( array(
+ 'url' => urlencode( home_url( '/testimonial/' ) ),
+ 'autofocus[section]' => 'jetpack_testimonials'
+ ), 'customize.php' )
+ );
+ }
+
+ /**
+ * Adds testimonial section to the Customizer.
+ */
+ function customize_register( $wp_customize ) {
+ jetpack_testimonial_custom_control_classes();
+
+ $wp_customize->add_section( 'jetpack_testimonials', array(
+ 'title' => esc_html__( 'Testimonials', 'jetpack' ),
+ 'theme_supports' => self::CUSTOM_POST_TYPE,
+ 'priority' => 130,
+ ) );
+
+ $wp_customize->add_setting( 'jetpack_testimonials[page-title]', array(
+ 'default' => esc_html__( 'Testimonials', 'jetpack' ),
+ 'sanitize_callback' => array( 'Jetpack_Testimonial_Title_Control', 'sanitize_content' ),
+ 'sanitize_js_callback' => array( 'Jetpack_Testimonial_Title_Control', 'sanitize_content' ),
+ ) );
+ $wp_customize->add_control( 'jetpack_testimonials[page-title]', array(
+ 'section' => 'jetpack_testimonials',
+ 'label' => esc_html__( 'Testimonial Archive Title', 'jetpack' ),
+ 'type' => 'text',
+ ) );
+
+ $wp_customize->add_setting( 'jetpack_testimonials[page-content]', array(
+ 'default' => '',
+ 'sanitize_callback' => array( 'Jetpack_Testimonial_Textarea_Control', 'sanitize_content' ),
+ 'sanitize_js_callback' => array( 'Jetpack_Testimonial_Textarea_Control', 'sanitize_content' ),
+ ) );
+ $wp_customize->add_control( new Jetpack_Testimonial_Textarea_Control( $wp_customize, 'jetpack_testimonials[page-content]', array(
+ 'section' => 'jetpack_testimonials',
+ 'settings' => 'jetpack_testimonials[page-content]',
+ 'label' => esc_html__( 'Testimonial Archive Content', 'jetpack' ),
+ ) ) );
+
+ $wp_customize->add_setting( 'jetpack_testimonials[featured-image]', array(
+ 'default' => '',
+ 'sanitize_callback' => 'attachment_url_to_postid',
+ 'sanitize_js_callback' => 'attachment_url_to_postid',
+ 'theme_supports' => 'post-thumbnails',
+ ) );
+ $wp_customize->add_control( new WP_Customize_Image_Control( $wp_customize, 'jetpack_testimonials[featured-image]', array(
+ 'section' => 'jetpack_testimonials',
+ 'label' => esc_html__( 'Testimonial Archive Featured Image', 'jetpack' ),
+ ) ) );
+
+ // The featured image control doesn't display properly in the Customizer unless we coerce
+ // it back into a URL sooner, since that's what WP_Customize_Upload_Control::to_json() expects
+ if ( is_admin() ) {
+ add_filter( 'theme_mod_jetpack_testimonials', array( $this, 'coerce_testimonial_image_to_url' ) );
+ }
+ }
+
+ public function coerce_testimonial_image_to_url( $opt ) {
+ if ( ! $opt || ! is_array( $opt ) ) {
+ return $opt;
+ }
+ if ( ! isset( $opt['featured-image'] ) || ! is_scalar( $opt['featured-image'] ) ) {
+ return $opt;
+ }
+ $url = wp_get_attachment_url( $opt['featured-image'] );
+ if ( $url ) {
+ $opt['featured-image'] = $url;
+ }
+ return $opt;
+ }
+
+ /**
+ * Our [testimonial] shortcode.
+ * Prints Testimonial data styled to look good on *any* theme.
+ *
+ * @return jetpack_testimonial_shortcode_html
+ */
+ static function jetpack_testimonial_shortcode( $atts ) {
+ // Default attributes
+ $atts = shortcode_atts( array(
+ 'display_content' => true,
+ 'image' => true,
+ 'columns' => 1,
+ 'showposts' => -1,
+ 'order' => 'asc',
+ 'orderby' => 'menu_order,date',
+ ), $atts, 'testimonial' );
+
+ // A little sanitization
+ if ( $atts['display_content'] && 'true' != $atts['display_content'] && 'full' != $atts['display_content'] ) {
+ $atts['display_content'] = false;
+ }
+
+ if ( $atts['image'] && 'true' != $atts['image'] ) {
+ $atts['image'] = false;
+ }
+
+ $atts['columns'] = absint( $atts['columns'] );
+
+ $atts['showposts'] = intval( $atts['showposts'] );
+
+ if ( $atts['order'] ) {
+ $atts['order'] = urldecode( $atts['order'] );
+ $atts['order'] = strtoupper( $atts['order'] );
+ if ( 'DESC' != $atts['order'] ) {
+ $atts['order'] = 'ASC';
+ }
+ }
+
+ if ( $atts['orderby'] ) {
+ $atts['orderby'] = urldecode( $atts['orderby'] );
+ $atts['orderby'] = strtolower( $atts['orderby'] );
+ $allowed_keys = array( 'author', 'date', 'title', 'menu_order', 'rand' );
+
+ $parsed = array();
+ foreach ( explode( ',', $atts['orderby'] ) as $testimonial_index_number => $orderby ) {
+ if ( ! in_array( $orderby, $allowed_keys ) ) {
+ continue;
+ }
+ $parsed[] = $orderby;
+ }
+
+ if ( empty( $parsed ) ) {
+ unset($atts['orderby']);
+ } else {
+ $atts['orderby'] = implode( ' ', $parsed );
+ }
+ }
+
+ // enqueue shortcode styles when shortcode is used
+ wp_enqueue_style( 'jetpack-testimonial-style', plugins_url( 'css/testimonial-shortcode.css', __FILE__ ), array(), '20140326' );
+
+ return self::jetpack_testimonial_shortcode_html( $atts );
+ }
+
+ /**
+ * The Testimonial shortcode loop.
+ *
+ * @return html
+ */
+ static function jetpack_testimonial_shortcode_html( $atts ) {
+ // Default query arguments
+ $defaults = array(
+ 'order' => $atts['order'],
+ 'orderby' => $atts['orderby'],
+ 'posts_per_page' => $atts['showposts'],
+ );
+
+ $args = wp_parse_args( $atts, $defaults );
+ $args['post_type'] = self::CUSTOM_POST_TYPE; // Force this post type
+ $query = new WP_Query( $args );
+
+ $testimonial_index_number = 0;
+
+ ob_start();
+
+ // If we have testimonials, create the html
+ if ( $query->have_posts() ) {
+
+ ?>
+ <div class="jetpack-testimonial-shortcode column-<?php echo esc_attr( $atts['columns'] ); ?>">
+ <?php // open .jetpack-testimonial-shortcode
+
+ // Construct the loop...
+ while ( $query->have_posts() ) {
+ $query->the_post();
+ $post_id = get_the_ID();
+ ?>
+ <div class="testimonial-entry <?php echo esc_attr( self::get_testimonial_class( $testimonial_index_number, $atts['columns'], has_post_thumbnail( $post_id ) ) ); ?>">
+ <?php
+ // The content
+ if ( false !== $atts['display_content'] ) {
+ if ( 'full' === $atts['display_content'] ) {
+ ?>
+ <div class="testimonial-entry-content"><?php the_content(); ?></div>
+ <?php
+ } else {
+ ?>
+ <div class="testimonial-entry-content"><?php the_excerpt(); ?></div>
+ <?php
+ }
+ }
+ ?>
+ <span class="testimonial-entry-title">&#8213; <a href="<?php echo esc_url( get_permalink() ); ?>" title="<?php echo esc_attr( the_title_attribute( ) ); ?>"><?php the_title(); ?></a></span>
+ <?php
+ // Featured image
+ if ( false !== $atts['image'] ) :
+ echo self::get_testimonial_thumbnail_link( $post_id );
+ endif;
+ ?>
+ </div><!-- close .testimonial-entry -->
+ <?php
+ $testimonial_index_number++;
+ } // end of while loop
+
+ wp_reset_postdata();
+ ?>
+ </div><!-- close .jetpack-testimonial-shortcode -->
+ <?php
+ } else { ?>
+ <p><em><?php _e( 'Your Testimonial Archive currently has no entries. You can start creating them on your dashboard.', 'jetpack' ); ?></p></em>
+ <?php
+ }
+ $html = ob_get_clean();
+
+ // Return the HTML block
+ return $html;
+ }
+
+ /**
+ * Individual testimonial class
+ *
+ * @return string
+ */
+ static function get_testimonial_class( $testimonial_index_number, $columns, $image ) {
+ $class = array();
+
+ $class[] = 'testimonial-entry-column-'.$columns;
+
+ if( $columns > 1) {
+ if ( ( $testimonial_index_number % 2 ) == 0 ) {
+ $class[] = 'testimonial-entry-mobile-first-item-row';
+ } else {
+ $class[] = 'testimonial-entry-mobile-last-item-row';
+ }
+ }
+
+ // add first and last classes to first and last items in a row
+ if ( ( $testimonial_index_number % $columns ) == 0 ) {
+ $class[] = 'testimonial-entry-first-item-row';
+ } elseif ( ( $testimonial_index_number % $columns ) == ( $columns - 1 ) ) {
+ $class[] = 'testimonial-entry-last-item-row';
+ }
+
+ // add class if testimonial has a featured image
+ if ( false !== $image ) {
+ $class[] = 'has-testimonial-thumbnail';
+ }
+
+ /**
+ * Filter the class applied to testimonial div in the testimonial
+ *
+ * @module custom-content-types
+ *
+ * @since 3.4.0
+ *
+ * @param string $class class name of the div.
+ * @param int $testimonial_index_number iterator count the number of columns up starting from 0.
+ * @param int $columns number of columns to display the content in.
+ * @param boolean $image has a thumbnail or not.
+ *
+ */
+ return apply_filters( 'testimonial-entry-post-class', implode( " ", $class ) , $testimonial_index_number, $columns, $image );
+ }
+
+ /**
+ * Display the featured image if it's available
+ *
+ * @return html
+ */
+ static function get_testimonial_thumbnail_link( $post_id ) {
+ if ( has_post_thumbnail( $post_id ) ) {
+ /**
+ * Change the thumbnail size for the Testimonial CPT.
+ *
+ * @module custom-content-types
+ *
+ * @since 3.4.0
+ *
+ * @param string|array $var Either a registered size keyword or size array.
+ */
+ return '<a class="testimonial-featured-image" href="' . esc_url( get_permalink( $post_id ) ) . '">' . get_the_post_thumbnail( $post_id, apply_filters( 'jetpack_testimonial_thumbnail_size', 'thumbnail' ) ) . '</a>';
+ }
+ }
+}
+
+function jetpack_testimonial_custom_control_classes() {
+ class Jetpack_Testimonial_Title_Control extends WP_Customize_Control {
+ public static function sanitize_content( $value ) {
+ if ( '' != $value )
+ $value = trim( convert_chars( wptexturize( $value ) ) );
+
+ return $value;
+ }
+ }
+
+ class Jetpack_Testimonial_Textarea_Control extends WP_Customize_Control {
+ public $type = 'textarea';
+
+ public function render_content() {
+ ?>
+ <label>
+ <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
+ <textarea rows="5" style="width:100%;" <?php $this->link(); ?>><?php echo esc_textarea( $this->value() ); ?></textarea>
+ </label>
+ <?php
+ }
+
+ public static function sanitize_content( $value ) {
+ if ( ! empty( $value ) )
+ /** This filter is already documented in core. wp-includes/post-template.php */
+ $value = apply_filters( 'the_content', $value );
+
+ $value = preg_replace( '@<div id="jp-post-flair"([^>]+)?>(.+)?</div>@is', '', $value ); // Strip WPCOM and Jetpack post flair if included in content
+
+ return $value;
+ }
+ }
+}
+
+add_action( 'init', array( 'Jetpack_Testimonial', 'init' ) );
+
+// Check on plugin activation if theme supports CPT
+register_activation_hook( __FILE__, array( 'Jetpack_Testimonial', 'activation_post_type_support' ) );
+add_action( 'jetpack_activate_module_custom-content-types', array( 'Jetpack_Testimonial', 'activation_post_type_support' ) );
diff --git a/plugins/jetpack/modules/debug.php b/plugins/jetpack/modules/debug.php
new file mode 100644
index 00000000..88c13090
--- /dev/null
+++ b/plugins/jetpack/modules/debug.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer needed.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/enhanced-distribution.php b/plugins/jetpack/modules/enhanced-distribution.php
new file mode 100644
index 00000000..43c7f31d
--- /dev/null
+++ b/plugins/jetpack/modules/enhanced-distribution.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Module Name: Enhanced Distribution
+ * Module Description: Increase reach and traffic.
+ * Sort Order: 5
+ * First Introduced: 1.2
+ * Requires Connection: Yes
+ * Auto Activate: Public
+ * Module Tags: Writing
+ * Feature: Engagement
+ * Additional Search Queries: google, seo, firehose, search, broadcast, broadcasting
+ */
+
+// In case it's active prior to upgrading to 1.9
+function jetpack_enhanced_distribution_before_activate_default_modules() {
+ $old_version = Jetpack_Options::get_option( 'old_version' );
+ list( $old_version ) = explode( ':', $old_version );
+
+ if ( version_compare( $old_version, '1.9-something', '>=' ) ) {
+ return;
+ }
+
+ Jetpack::check_privacy( __FILE__ );
+}
+
+add_action( 'jetpack_before_activate_default_modules', 'jetpack_enhanced_distribution_before_activate_default_modules' );
+
+/**
+ * If a request has ?get_freshly_pressed_data=true appended
+ * to the end, then let's provide the necessary data back via JSON.
+ */
+if ( isset( $_GET['get_freshly_pressed_data'] ) ) {
+ add_action( 'template_redirect', 'jetpack_get_freshly_pressed_data' );
+ function jetpack_get_freshly_pressed_data() {
+ if ( is_single() ) {
+ wp_send_json_success( array(
+ 'blog_id' => Jetpack_Options::get_option( 'id' ),
+ 'post_id' => get_the_ID(),
+ ) );
+ } else {
+ wp_send_json_error( array(
+ 'message' => 'Not Singular',
+ ) );
+ }
+ }
+}
+
+add_action( 'rss_head', 'jetpack_enhanced_distribution_feed_id' );
+add_action( 'rss_item', 'jetpack_enhanced_distribution_post_id' );
+add_action( 'rss2_head', 'jetpack_enhanced_distribution_feed_id' );
+add_action( 'rss2_item', 'jetpack_enhanced_distribution_post_id' );
+
+function jetpack_enhanced_distribution_feed_id(){
+ (int) $id = Jetpack_Options::get_option( 'id' );
+ if ( $id > 0 ) {
+ $output = sprintf( '<site xmlns="com-wordpress:feed-additions:1">%d</site>', $id );
+ echo $output;
+ }
+}
+
+function jetpack_enhanced_distribution_post_id(){
+ $id = get_the_ID();
+ if ( $id ) {
+ $output = sprintf( '<post-id xmlns="com-wordpress:feed-additions:1">%d</post-id>', $id );
+ echo $output;
+ }
+}
diff --git a/plugins/jetpack/modules/geo-location.php b/plugins/jetpack/modules/geo-location.php
new file mode 100644
index 00000000..4d3e255c
--- /dev/null
+++ b/plugins/jetpack/modules/geo-location.php
@@ -0,0 +1,81 @@
+<?php
+
+require_once dirname( __FILE__ ) . '/geo-location/class.jetpack-geo-location.php';
+
+/**
+ * Geo-location shortcode for display of location data associated with a post.
+ *
+ * Usage with current global $post:
+ * [geo-location]
+ *
+ * Usage with specific post ID:
+ * [geo-location post=5]
+ */
+add_shortcode( 'geo-location', 'jetpack_geo_shortcode' );
+
+function jetpack_geo_shortcode( $attributes ) {
+ $attributes = shortcode_atts( array( 'post' => null, 'id' => null ), $attributes );
+ return jetpack_geo_get_location( $attributes['post'] ? $attributes['post'] : $attributes['id'] );
+}
+
+/**
+ * Get the geo-location data associated with the supplied post ID, if it's available
+ * and marked as being available for public display. The returned array will contain
+ * "latitude", "longitude" and "label" keys.
+ *
+ * If you do not supply a value for $post_id, the global $post will be used, if
+ * available.
+ *
+ * @param integer|null $post_id
+ *
+ * @return array|null
+ */
+function jetpack_geo_get_data( $post_id = null) {
+ $geo = Jetpack_Geo_Location::init();
+
+ if ( ! $post_id ) {
+ $post_id = $geo->get_post_id();
+ }
+
+ $meta_values = $geo->get_meta_values( $post_id );
+
+ if ( ! $meta_values['is_public'] || ! $meta_values['is_populated'] ) {
+ return null;
+ }
+
+ return array(
+ 'latitude' => $meta_values['latitude'],
+ 'longitude' => $meta_values['longitude'],
+ 'label' => $meta_values['label']
+ );
+}
+
+/**
+ * Display the label HTML for the geo-location information associated with the supplied
+ * post ID.
+ *
+ * If you do not supply a value for $post_id, the global $post will be used, if
+ * available.
+ *
+ * @param integer|null $post_id
+ *
+ * @return void
+ */
+function jetpack_geo_display_location( $post_id = null ) {
+ echo jetpack_geo_get_location( $post_id );
+}
+
+/**
+ * Return the label HTML for the geo-location information associated with the supplied
+ * post ID.
+ *
+ * If you do not supply a value for $post_id, the global $post will be used, if
+ * available.
+ *
+ * @param integer|null $post_id
+ *
+ * @return string
+ */
+function jetpack_geo_get_location( $post_id = null ) {
+ return Jetpack_Geo_Location::init()->get_location_label( $post_id );
+}
diff --git a/plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php b/plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php
new file mode 100644
index 00000000..e4941537
--- /dev/null
+++ b/plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php
@@ -0,0 +1,425 @@
+<?php
+
+/**
+ * Adds support for geo-location features.
+ *
+ * All Jetpack sites can support geo-location features. Users can tag posts with geo-location data
+ * using the UI provided by Calypso. That information will be included in RSS feeds, meta tags during
+ * wp_head, and in the Geo microformat following post content.
+ *
+ * If your theme declares support for "geo-location", you'll also get a small icon and location label
+ * visible to users at the bottom of single posts and pages.
+ *
+ * To declare support in your theme, call `add_theme_support( 'jetpack-geo-location' )`.
+ *
+ * Once you've added theme support, you can rely on the standard HTML output generated in the
+ * the_content_location_display() method of this class. Or, you can use the "geo_location_display"
+ * filter to generate custom HTML for your particular theme. Your filter function will receive an
+ * the default HTML as its first argument and an array containing the geo-location information as
+ * its second argument in the following format:
+ *
+ * array(
+ * 'is_public' => boolean,
+ * 'latitude' => float,
+ * 'longitude' => float,
+ * 'label' => string,
+ * 'is_populated' => boolean
+ * )
+ *
+ * Add your filter with:
+ *
+ * add_filter( 'jetpack_geo_location_display', 'your_filter_function_name', 10, 2);
+ */
+class Jetpack_Geo_Location {
+ private static $instance;
+
+ /**
+ * Whether dashicons are enqueued.
+ *
+ * @since 6.6.0
+ *
+ * @var bool
+ */
+ private static $style_enqueued = false;
+
+ public static function init() {
+ if ( is_null( self::$instance ) ) {
+ self::$instance = new Jetpack_Geo_Location();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * This is mostly just used for testing purposes.
+ */
+ public static function reset_instance() {
+ self::$instance = null;
+ }
+
+ public function __construct() {
+ add_action( 'init', array( $this, 'wordpress_init' ) );
+ add_action( 'wp_head', array( $this, 'wp_head' ) );
+ add_filter( 'the_content', array( $this, 'the_content_microformat' ) );
+
+ $this->register_rss_hooks();
+ }
+
+ /**
+ * Register support for the geo-location feature on pages and posts. Register the meta
+ * fields managed by this plugin so that they are properly sanitized during save.
+ */
+ public function wordpress_init() {
+ // Only render location label after post content, if the theme claims to support "geo-location".
+ if ( current_theme_supports( 'jetpack-geo-location' ) ) {
+ add_filter( 'the_content', array( $this, 'the_content_location_display' ), 15, 1 );
+ }
+
+ add_post_type_support( 'post', 'geo-location' );
+ add_post_type_support( 'page', 'geo-location' );
+
+ register_meta(
+ 'post',
+ 'geo_public',
+ array(
+ 'sanitize_callback' => array( $this, 'sanitize_public' ),
+ 'type' => 'boolean',
+ 'single' => true,
+ )
+ );
+
+ register_meta(
+ 'post',
+ 'geo_latitude',
+ array(
+ 'sanitize_callback' => array( $this, 'sanitize_coordinate' ),
+ 'type' => 'float',
+ 'single' => true,
+ )
+ );
+
+ register_meta(
+ 'post',
+ 'geo_longitude',
+ array(
+ 'sanitize_callback' => array( $this, 'sanitize_coordinate' ),
+ 'type' => 'float',
+ 'single' => true,
+ )
+ );
+
+ register_meta(
+ 'post',
+ 'geo_address',
+ array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'type' => 'string',
+ 'single' => true,
+ )
+ );
+ }
+
+ /**
+ * Filter "public" input to always be either 1 or 0.
+ *
+ * @param mixed $public
+ *
+ * @return int
+ */
+ public function sanitize_public( $public ) {
+ return absint( $public ) ? 1 : 0;
+ }
+
+ /**
+ * Filter geo coordinates and normalize them to floats with 7 digits of precision.
+ *
+ * @param mixed $coordinate
+ *
+ * @return float|null
+ */
+ public function sanitize_coordinate( $coordinate ) {
+ if ( ! $coordinate ) {
+ return null;
+ }
+
+ return round( (float) $coordinate, 7 );
+ }
+
+ /**
+ * Render geo.position and ICBM meta tags with public geo meta values when rendering
+ * a single post.
+ */
+ public function wp_head() {
+ if ( ! is_single() ) {
+ return;
+ }
+
+ $meta_values = $this->get_meta_values( $this->get_post_id() );
+
+ if ( ! $meta_values['is_public'] ) {
+ return;
+ }
+
+ if ( ! self::$style_enqueued ) {
+ // only enqueue scripts and styles when needed.
+ self::enqueue_scripts();
+ self::$style_enqueued = true;
+ }
+
+ echo "\n<!-- Jetpack Geo-location Tags -->\n";
+
+ if ( $meta_values['label'] ) {
+ printf(
+ '<meta name="geo.placename" content="%s" />',
+ esc_attr( $meta_values['label'] )
+ );
+ }
+
+ printf(
+ '<meta name="geo.position" content="%s;%s" />' . PHP_EOL,
+ esc_attr( $meta_values['latitude'] ),
+ esc_attr( $meta_values['longitude'] )
+ );
+
+ printf(
+ '<meta name="ICBM" content="%s, %s" />' . PHP_EOL,
+ esc_attr( $meta_values['latitude'] ),
+ esc_attr( $meta_values['longitude'] )
+ );
+
+ echo "\n<!-- End Jetpack Geo-location Tags -->\n";
+ }
+
+ /**
+ * Append public meta values in the Geo microformat (https://en.wikipedia.org/wiki/Geo_(microformat)
+ * to the supplied content.
+ *
+ * Note that we cannot render the microformat in the context of an excerpt because tags are stripped
+ * in that context, making our microformat data visible.
+ *
+ * @param string $content
+ *
+ * @return string
+ */
+ public function the_content_microformat( $content ) {
+ if ( is_feed() || $this->is_currently_excerpt_filter() ) {
+ return $content;
+ }
+
+ $meta_values = $this->get_meta_values( $this->get_post_id() );
+
+ if ( ! $meta_values['is_public'] ) {
+ return $content;
+ }
+
+ $microformat = sprintf(
+ '<div id="geo-post-%d" class="geo geo-post" style="display: none">',
+ esc_attr( $this->get_post_id() )
+ );
+
+ $microformat .= sprintf(
+ '<span class="latitude">%s</span>',
+ esc_html( $meta_values['latitude'] )
+ );
+
+ $microformat .= sprintf(
+ '<span class="longitude">%s</span>',
+ esc_html( $meta_values['longitude'] )
+ );
+
+ $microformat .= '</div>';
+
+ return $content . $microformat;
+ }
+
+ /**
+ * Register a range of hooks for integrating geo data with various feeds.
+ */
+ public function register_rss_hooks() {
+ add_action( 'rss2_ns', array( $this, 'rss_namespace' ) );
+ add_action( 'atom_ns', array( $this, 'rss_namespace' ) );
+ add_action( 'rdf_ns', array( $this, 'rss_namespace' ) );
+ add_action( 'rss_item', array( $this, 'rss_item' ) );
+ add_action( 'rss2_item', array( $this, 'rss_item' ) );
+ add_action( 'atom_entry', array( $this, 'rss_item' ) );
+ add_action( 'rdf_item', array( $this, 'rss_item' ) );
+ }
+
+ /**
+ * Add the georss namespace during RSS generation.
+ */
+ public function rss_namespace() {
+ echo PHP_EOL . 'xmlns:georss="http://www.georss.org/georss" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"' . PHP_EOL;
+ }
+
+ /**
+ * Output georss data for RSS items, assuming we have data for the currently rendered post and
+ * that data as marked as public.
+ */
+ public function rss_item() {
+ $meta_values = $this->get_meta_values( $this->get_post_id() );
+
+ if ( ! $meta_values['is_public'] ) {
+ return;
+ }
+
+ printf(
+ "\t<georss:point>%s %s</georss:point>\n",
+ ent2ncr( esc_html( $meta_values['latitude'] ) ),
+ ent2ncr( esc_html( $meta_values['longitude'] ) )
+ );
+
+ printf( "\t\t<geo:lat>%s</geo:lat>\n", ent2ncr( esc_html( $meta_values['latitude'] ) ) );
+ printf( "\t\t<geo:long>%s</geo:long>\n", ent2ncr( esc_html( $meta_values['longitude'] ) ) );
+ }
+
+ /**
+ * Enqueue CSS for rendering post flair with geo-location.
+ */
+ private static function enqueue_scripts() {
+ wp_enqueue_style( 'dashicons' );
+ }
+
+ /**
+ * If we're rendering a single post and public geo-location data is available for it,
+ * include the human-friendly location label in the output.
+ *
+ * @param string $content
+ *
+ * @return string
+ */
+ public function the_content_location_display( $content ) {
+ if ( ! is_single() ) {
+ return $content;
+ }
+
+ return $content . $this->get_location_label();
+ }
+
+ /**
+ * Get the HTML for displaying a label representing the location associated with the
+ * supplied post ID. If no post ID is given, we'll use the global $post variable, if
+ * it is available.
+ *
+ * @param integer|null $post_id
+ *
+ * @return string
+ */
+ public function get_location_label( $post_id = null ) {
+ $meta_values = $this->get_meta_values( $post_id ? $post_id : $this->get_post_id() );
+
+ if ( ! $meta_values['is_public'] ) {
+ return '';
+ }
+
+ // If the location has not been labeled, do not show the location.
+ if ( ! $meta_values['label'] ) {
+ return '';
+ }
+
+ $html = '<div class="post-geo-location-label geo-chip">';
+ $html .= '<span class="dashicons dashicons-location" style="vertical-align: text-top;"></span> ';
+ $html .= esc_html( $meta_values['label'] );
+ $html .= '</div>';
+
+ /**
+ * Allow modification or replacement of the default geo-location display HTML.
+ *
+ * @module geo-location
+ *
+ * @param array $html The default HTML for displaying a geo-location label.
+ * @param array $geo_data An array containing "latitude", "longitude" and "label".
+ */
+ $html = apply_filters( 'jetpack_geo_location_display', $html, $meta_values );
+
+ return $html;
+ }
+
+ /**
+ * Get the ID of the current global post object, if available. Otherwise, return null.
+ *
+ * This isolates the access of the global scope to this single method, making it easier to
+ * safeguard against unexpected missing $post objects in other hook functions.
+ *
+ * @return int|null
+ */
+ public function get_post_id() {
+ global $post;
+
+ if ( ! isset( $post ) || ! $post || ! is_object( $post ) || ! isset( $post->ID ) ) {
+ return null;
+ }
+
+ return $post->ID;
+ }
+
+ /**
+ * This method always returns an array with the following structure:
+ *
+ * array(is_public => bool, latitude => float, longitude => float, label => string, is_populated => bool)
+ *
+ * So, regardless of whether your post actually has values in postmeta for the geo-location fields,
+ * you can be sure that you can reference those array keys in calling code without having to juggle
+ * isset(), array_key_exists(), etc.
+ *
+ * Mocking this method during testing can also be useful for testing output and logic in various
+ * hook functions.
+ *
+ * @param integer $post_id
+ *
+ * @return array A predictably structured array representing the meta values for the supplied post ID.
+ */
+ public function get_meta_values( $post_id ) {
+ $meta_values = array(
+ 'is_public' => (bool) $this->sanitize_public( $this->get_meta_value( $post_id, 'public' ) ),
+ 'latitude' => $this->sanitize_coordinate( $this->get_meta_value( $post_id, 'latitude' ) ),
+ 'longitude' => $this->sanitize_coordinate( $this->get_meta_value( $post_id, 'longitude' ) ),
+ 'label' => trim( $this->get_meta_value( $post_id, 'address' ) ),
+ 'is_populated' => false,
+ );
+
+ if ( $meta_values['latitude'] && $meta_values['longitude'] && $meta_values['label'] ) {
+ $meta_values['is_populated'] = true;
+ }
+
+ return $meta_values;
+ }
+
+ /**
+ * This function wraps get_post_meta() to enable us to keep the "geo_" prefix isolated to a single
+ * location in the code and to assist in mocking during testing.
+ *
+ * @param integer $post_id
+ * @param string $meta_field_name
+ *
+ * @return mixed
+ */
+ public function get_meta_value( $post_id, $meta_field_name ) {
+ if ( ! $post_id ) {
+ return null;
+ }
+
+ return get_post_meta( $post_id, 'geo_' . $meta_field_name, true );
+ }
+
+ /**
+ * Check to see if the current filter is the get_the_excerpt filter.
+ *
+ * Just checking current_filter() here is not adequate because current_filter() only looks
+ * at the last element in the $wp_current_filter array. In the context of rendering an
+ * excerpt, however, both get_the_excerpt and the_content are present in that array.
+ *
+ * @return bool
+ */
+ public function is_currently_excerpt_filter() {
+ if ( ! isset( $GLOBALS['wp_current_filter'] ) ) {
+ return false;
+ }
+
+ $current_filters = (array) $GLOBALS['wp_current_filter'];
+
+ return in_array( 'get_the_excerpt', $current_filters, true );
+ }
+}
+
+Jetpack_Geo_Location::init();
diff --git a/plugins/jetpack/modules/google-analytics.php b/plugins/jetpack/modules/google-analytics.php
new file mode 100644
index 00000000..21a42921
--- /dev/null
+++ b/plugins/jetpack/modules/google-analytics.php
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * Module Name: Google Analytics
+ * Module Description: Set up Google Analytics without touching a line of code.
+ * First Introduced: 4.5
+ * Sort Order: 37
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Feature: Engagement
+ * Additional Search Queries: webmaster, google, analytics, console
+ * Plans: business, premium
+ */
+
+include dirname( __FILE__ ) . "/google-analytics/wp-google-analytics.php";
diff --git a/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-legacy.php b/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-legacy.php
new file mode 100644
index 00000000..ddca0e46
--- /dev/null
+++ b/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-legacy.php
@@ -0,0 +1,256 @@
+<?php
+
+/**
+* Jetpack_Google_Analytics_Legacy hooks and enqueues support for ga.js
+* https://developers.google.com/analytics/devguides/collection/gajs/
+*
+* @author Aaron D. Campbell (original)
+* @author allendav
+*/
+
+/**
+* Bail if accessed directly
+*/
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+class Jetpack_Google_Analytics_Legacy {
+ public function __construct() {
+ add_filter( 'jetpack_wga_classic_custom_vars', array( $this, 'jetpack_wga_classic_anonymize_ip' ) );
+ add_filter( 'jetpack_wga_classic_custom_vars', array( $this, 'jetpack_wga_classic_track_purchases' ) );
+ add_action( 'wp_footer', array( $this, 'insert_code' ) );
+ add_action( 'wp_footer', array( $this, 'jetpack_wga_classic_track_add_to_cart' ) );
+ }
+
+ /**
+ * Used to generate a tracking URL
+ * Called exclusively by insert_code
+ *
+ * @param array $track - Must have ['data'] and ['code'].
+ * @return string - Tracking URL
+ */
+ private function _get_url( $track ) {
+ $site_url = ( is_ssl() ? 'https://':'http://' ) . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ); // Input var okay.
+ foreach ( $track as $k => $value ) {
+ if ( strpos( strtolower( $value ), strtolower( $site_url ) ) === 0 ) {
+ $track[ $k ] = substr( $track[ $k ], strlen( $site_url ) );
+ }
+ if ( 'data' === $k ) {
+ $track[ $k ] = preg_replace( '/^https?:\/\/|^\/+/i', '', $track[ $k ] );
+ }
+
+ // This way we don't lose search data.
+ if ( 'data' === $k && 'search' === $track['code'] ) {
+ $track[ $k ] = rawurlencode( $track[ $k ] );
+ } else {
+ $track[ $k ] = preg_replace( '/[^a-z0-9\.\/\+\?=-]+/i', '_', $track[ $k ] );
+ }
+
+ $track[ $k ] = trim( $track[ $k ], '_' );
+ }
+ $char = ( strpos( $track['data'], '?' ) === false ) ? '?' : '&amp;';
+ return str_replace( "'", "\'", "/{$track['code']}/{$track['data']}{$char}referer=" . rawurlencode( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : '' ) ); // Input var okay.
+ }
+
+ /**
+ * This injects the Google Analytics code into the footer of the page.
+ * Called exclusively by wp_footer action
+ */
+ public function insert_code() {
+ $tracking_id = Jetpack_Google_Analytics_Options::get_tracking_code();
+ if ( empty( $tracking_id ) ) {
+ echo "<!-- Your Google Analytics Plugin is missing the tracking ID -->\r\n";
+ return;
+ }
+
+ // If we're in the admin_area, return without inserting code.
+ if ( is_admin() ) {
+ return;
+ }
+
+ $custom_vars = array(
+ "_gaq.push(['_setAccount', '{$tracking_id}']);",
+ );
+
+ $track = array();
+ if ( is_404() ) {
+ // This is a 404 and we are supposed to track them.
+ $custom_vars[] = "_gaq.push(['_trackEvent', '404', document.location.href, document.referrer]);";
+ } elseif (
+ is_search()
+ && isset( $_REQUEST['s'] )
+ ) {
+ // Set track for searches, if it's a search, and we are supposed to.
+ $track['data'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // Input var okay.
+ $track['code'] = 'search';
+ }
+
+ if ( ! empty( $track ) ) {
+ $track['url'] = $this->_get_url( $track );
+ // adjust the code that we output, account for both types of tracking.
+ $track['url'] = esc_js( str_replace( '&', '&amp;', $track['url'] ) );
+ $custom_vars[] = "_gaq.push(['_trackPageview','{$track['url']}']);";
+ } else {
+ $custom_vars[] = "_gaq.push(['_trackPageview']);";
+ }
+
+ /**
+ * Allow for additional elements to be added to the classic Google Analytics queue (_gaq) array
+ *
+ * @since 5.4.0
+ *
+ * @param array $custom_vars Array of classic Google Analytics queue elements
+ */
+ $custom_vars = apply_filters( 'jetpack_wga_classic_custom_vars', $custom_vars );
+
+ // Ref: https://developers.google.com/analytics/devguides/collection/gajs/gaTrackingEcommerce#Example
+ printf(
+ "<!-- Jetpack Google Analytics -->
+ <script type='text/javascript'>
+ var _gaq = _gaq || [];
+ %s
+ (function() {
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ ga.src = ('https:' === document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+ })();
+ </script>\r\n",
+ implode( "\r\n", $custom_vars )
+ );
+ }
+
+ /**
+ * Used to filter in the anonymize IP snippet to the custom vars array for classic analytics
+ * Ref https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApi_gat#_gat._anonymizelp
+ * @param array custom vars to be filtered
+ * @return array possibly updated custom vars
+ */
+ public function jetpack_wga_classic_anonymize_ip( $custom_vars ) {
+ if ( Jetpack_Google_Analytics_Options::anonymize_ip_is_enabled() ) {
+ array_push( $custom_vars, "_gaq.push(['_gat._anonymizeIp']);" );
+ }
+
+ return $custom_vars;
+ }
+
+ /**
+ * Used to filter in the order details to the custom vars array for classic analytics
+ * @param array custom vars to be filtered
+ * @return array possibly updated custom vars
+ */
+ public function jetpack_wga_classic_track_purchases( $custom_vars ) {
+ global $wp;
+
+ if ( ! class_exists( 'WooCommerce' ) ) {
+ return $custom_vars;
+ }
+
+ if ( ! Jetpack_Google_Analytics_Options::has_tracking_code() ) {
+ return;
+ }
+
+ // Ref: https://developers.google.com/analytics/devguides/collection/gajs/gaTrackingEcommerce#Example
+ if ( ! Jetpack_Google_Analytics_Options::track_purchases_is_enabled() ) {
+ return $custom_vars;
+ }
+
+ $minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( WC_VERSION, '3.0', '>=' );
+ if ( $minimum_woocommerce_active && is_order_received_page() ) {
+ $order_id = isset( $wp->query_vars['order-received'] ) ? $wp->query_vars['order-received'] : 0;
+ if ( 0 < $order_id && 1 != get_post_meta( $order_id, '_ga_tracked', true ) ) {
+ $order = new WC_Order( $order_id );
+
+ // [ '_add_Trans', '123', 'Site Title', '21.00', '1.00', '5.00', 'Snohomish', 'WA', 'USA' ]
+ array_push(
+ $custom_vars,
+ sprintf(
+ '_gaq.push( %s );', json_encode(
+ array(
+ '_addTrans',
+ (string) $order->get_order_number(),
+ get_bloginfo( 'name' ),
+ (string) $order->get_total(),
+ (string) $order->get_total_tax(),
+ (string) $order->get_total_shipping(),
+ (string) $order->get_billing_city(),
+ (string) $order->get_billing_state(),
+ (string) $order->get_billing_country()
+ )
+ )
+ )
+ );
+
+ // Order items
+ if ( $order->get_items() ) {
+ foreach ( $order->get_items() as $item ) {
+ $product = $order->get_product_from_item( $item );
+ $product_sku_or_id = $product->get_sku() ? $product->get_sku() : $product->get_id();
+
+ array_push(
+ $custom_vars,
+ sprintf(
+ '_gaq.push( %s );', json_encode(
+ array(
+ '_addItem',
+ (string) $order->get_order_number(),
+ (string) $product_sku_or_id,
+ $item['name'],
+ Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
+ (string) $order->get_item_total( $item ),
+ (string) $item['qty']
+ )
+ )
+ )
+ );
+ }
+ } // get_items
+
+ // Mark the order as tracked
+ update_post_meta( $order_id, '_ga_tracked', 1 );
+ array_push( $custom_vars, "_gaq.push(['_trackTrans']);" );
+ } // order not yet tracked
+ } // is order received page
+
+ return $custom_vars;
+ }
+
+ /**
+ * Used to add footer javascript to track user clicking on add-to-cart buttons
+ * on single views (.single_add_to_cart_button) and list views (.add_to_cart_button)
+ */
+ public function jetpack_wga_classic_track_add_to_cart() {
+ if ( ! class_exists( 'WooCommerce' ) ) {
+ return;
+ }
+
+ if ( ! Jetpack_Google_Analytics_Options::has_tracking_code() ) {
+ return;
+ }
+
+ if ( ! Jetpack_Google_Analytics_Options::track_add_to_cart_is_enabled() ) {
+ return;
+ }
+
+ if ( is_product() ) { // product page
+ global $product;
+ $product_sku_or_id = $product->get_sku() ? $product->get_sku() : "#" + $product->get_id();
+ wc_enqueue_js(
+ "jQuery( function( $ ) {
+ $( '.single_add_to_cart_button' ).click( function() {
+ _gaq.push(['_trackEvent', 'Products', 'Add to Cart', '#" . esc_js( $product_sku_or_id ) . "']);
+ } );
+ } );"
+ );
+ } else if ( is_woocommerce() ) { // any other page that uses templates (like product lists, archives, etc)
+ wc_enqueue_js(
+ "jQuery( function( $ ) {
+ $( '.add_to_cart_button:not(.product_type_variable, .product_type_grouped)' ).click( function() {
+ var label = $( this ).data( 'product_sku' ) ? $( this ).data( 'product_sku' ) : '#' + $( this ).data( 'product_id' );
+ _gaq.push(['_trackEvent', 'Products', 'Add to Cart', label]);
+ } );
+ } );"
+ );
+ }
+ }
+}
diff --git a/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-options.php b/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-options.php
new file mode 100644
index 00000000..b6e208b7
--- /dev/null
+++ b/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-options.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+* Jetpack_Google_Analytics_Options provides a single interface to module options
+*
+* @author allendav
+*/
+
+/**
+* Bail if accessed directly
+*/
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+class Jetpack_Google_Analytics_Options {
+ public static function get_option( $option_name, $default = false ) {
+ $o = get_option( 'jetpack_wga' );
+ return isset( $o[ $option_name ] ) ? $o[ $option_name ] : $default;
+ }
+
+ public static function get_tracking_code() {
+ return self::get_option( 'code', '' );
+ }
+
+ public static function has_tracking_code() {
+ $code = self::get_tracking_code();
+ return ! empty( $code );
+ }
+
+ // Options used by both legacy and universal analytics
+ public static function anonymize_ip_is_enabled() {
+ return self::get_option( 'anonymize_ip' );
+ }
+
+ // eCommerce options used by both legacy and universal analytics
+ public static function track_purchases_is_enabled() {
+ return self::get_option( 'ec_track_purchases' );
+ }
+
+ public static function track_add_to_cart_is_enabled() {
+ return self::get_option( 'ec_track_add_to_cart' );
+ }
+
+ // Enhanced eCommerce options
+ public static function enhanced_ecommerce_tracking_is_enabled() {
+ return self::get_option( 'enh_ec_tracking' );
+ }
+
+ public static function track_remove_from_cart_is_enabled() {
+ return self::get_option( 'enh_ec_track_remove_from_cart' );
+ }
+
+ public static function track_product_impressions_is_enabled() {
+ return self::get_option( 'enh_ec_track_prod_impression' );
+ }
+
+ public static function track_product_clicks_is_enabled() {
+ return self::get_option( 'enh_ec_track_prod_click' );
+ }
+
+ public static function track_product_detail_view_is_enabled() {
+ return self::get_option( 'enh_ec_track_prod_detail_view' );
+ }
+
+ public static function track_checkout_started_is_enabled() {
+ return self::get_option( 'enh_ec_track_checkout_started' );
+ }
+}
+
diff --git a/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-universal.php b/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-universal.php
new file mode 100644
index 00000000..999fffbd
--- /dev/null
+++ b/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-universal.php
@@ -0,0 +1,410 @@
+<?php
+
+/**
+* Jetpack_Google_Analytics_Universal hooks and and enqueues support for analytics.js
+* https://developers.google.com/analytics/devguides/collection/analyticsjs/
+* https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce
+*
+* @author allendav
+*/
+
+/**
+* Bail if accessed directly
+*/
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+class Jetpack_Google_Analytics_Universal {
+ public function __construct() {
+ add_filter( 'jetpack_wga_universal_commands', array( $this, 'maybe_anonymize_ip' ) );
+ add_filter( 'jetpack_wga_universal_commands', array( $this, 'maybe_track_purchases' ) );
+
+ add_action( 'wp_head', array( $this, 'wp_head' ), 999999 );
+
+ add_action( 'woocommerce_after_add_to_cart_button', array( $this, 'add_to_cart' ) );
+ add_action( 'wp_footer', array( $this, 'loop_add_to_cart' ) );
+ add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart' ) );
+ add_action( 'woocommerce_after_mini_cart', array( $this, 'remove_from_cart' ) );
+ add_filter( 'woocommerce_cart_item_remove_link', array( $this, 'remove_from_cart_attributes' ), 10, 2 );
+ add_action( 'woocommerce_after_shop_loop_item', array( $this, 'listing_impression' ) );
+ add_action( 'woocommerce_after_shop_loop_item', array( $this, 'listing_click' ) );
+ add_action( 'woocommerce_after_single_product', array( $this, 'product_detail' ) );
+ add_action( 'woocommerce_after_checkout_form', array( $this, 'checkout_process' ) );
+
+ // we need to send a pageview command last - so we use priority 24 to add
+ // this command's JavaScript just before wc_print_js is called (pri 25)
+ add_action( 'wp_footer', array( $this, 'send_pageview_in_footer' ), 24 );
+ }
+
+ public function wp_head() {
+ $tracking_code = Jetpack_Google_Analytics_Options::get_tracking_code();
+ if ( empty( $tracking_code ) ) {
+ echo "<!-- No tracking ID configured for Jetpack Google Analytics -->\r\n";
+ return;
+ }
+
+ // If we're in the admin_area, return without inserting code.
+ if ( is_admin() ) {
+ return;
+ }
+
+ /**
+ * Allow for additional elements to be added to the universal Google Analytics queue (ga) array
+ *
+ * @since 5.6.0
+ *
+ * @param array $custom_vars Array of universal Google Analytics queue elements
+ */
+ $universal_commands = apply_filters( 'jetpack_wga_universal_commands', array() );
+
+ $async_code = "
+ <!-- Jetpack Google Analytics -->
+ <script>
+ window.ga = window.ga || function(){ ( ga.q = ga.q || [] ).push( arguments ) }; ga.l=+new Date;
+ ga( 'create', '%tracking_id%', 'auto' );
+ ga( 'require', 'ec' );
+ %universal_commands%
+ </script>
+ <script async src='https://www.google-analytics.com/analytics.js'></script>
+ <!-- End Jetpack Google Analytics -->
+ ";
+ $async_code = str_replace( '%tracking_id%', $tracking_code, $async_code );
+
+ $universal_commands_string = implode( "\r\n", $universal_commands );
+ $async_code = str_replace( '%universal_commands%', $universal_commands_string, $async_code );
+
+ echo "$async_code\r\n";
+ }
+
+ public function maybe_anonymize_ip( $command_array ) {
+ if ( Jetpack_Google_Analytics_Options::anonymize_ip_is_enabled() ) {
+ array_push( $command_array, "ga( 'set', 'anonymizeIp', true );" );
+ }
+
+ return $command_array;
+ }
+
+ public function maybe_track_purchases( $command_array ) {
+ global $wp;
+
+ if ( ! Jetpack_Google_Analytics_Options::track_purchases_is_enabled() ) {
+ return $command_array;
+ }
+
+ if ( ! class_exists( 'WooCommerce' ) ) {
+ return $command_array;
+ }
+
+ $minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( WC_VERSION, '3.0', '>=' );
+ if ( ! $minimum_woocommerce_active ) {
+ return $command_array;
+ }
+
+ if ( ! is_order_received_page() ) {
+ return $command_array;
+ }
+
+ $order_id = isset( $wp->query_vars['order-received'] ) ? $wp->query_vars['order-received'] : 0;
+ if ( 0 == $order_id ) {
+ return $command_array;
+ }
+
+ // A 1 indicates we've already tracked this order - don't do it again
+ if ( 1 == get_post_meta( $order_id, '_ga_tracked', true ) ) {
+ return $command_array;
+ }
+
+ $order = new WC_Order( $order_id );
+ $order_currency = $order->get_currency();
+ $command = "ga( 'set', '&cu', '" . esc_js( $order_currency ) . "' );";
+ array_push( $command_array, $command );
+
+ // Order items
+ if ( $order->get_items() ) {
+ foreach ( $order->get_items() as $item ) {
+ $product = $order->get_product_from_item( $item );
+ $product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
+
+ $item_details = array(
+ 'id' => $product_sku_or_id,
+ 'name' => $item['name'],
+ 'category' => Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
+ 'price' => $order->get_item_total( $item ),
+ 'quantity' => $item['qty'],
+ );
+ $command = "ga( 'ec:addProduct', " . wp_json_encode( $item_details ) . " );";
+ array_push( $command_array, $command );
+ }
+ }
+
+ // Order summary
+ $summary = array(
+ 'id' => $order->get_order_number(),
+ 'affiliation' => get_bloginfo( 'name' ),
+ 'revenue' => $order->get_total(),
+ 'tax' => $order->get_total_tax(),
+ 'shipping' => $order->get_total_shipping()
+ );
+ $command = "ga( 'ec:setAction', 'purchase', " . wp_json_encode( $summary ) . " );";
+ array_push( $command_array, $command );
+
+ update_post_meta( $order_id, '_ga_tracked', 1 );
+
+ return $command_array;
+ }
+
+ public function add_to_cart() {
+ if ( ! Jetpack_Google_Analytics_Options::track_add_to_cart_is_enabled() ) {
+ return;
+ }
+
+ if ( ! is_single() ) {
+ return;
+ }
+
+ global $product;
+
+ $product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
+ $selector = ".single_add_to_cart_button";
+
+ wc_enqueue_js(
+ "$( '" . esc_js( $selector ) . "' ).click( function() {
+ var productDetails = {
+ 'id': '" . esc_js( $product_sku_or_id ) . "',
+ 'name' : '" . esc_js( $product->get_title() ) . "',
+ 'quantity': $( 'input.qty' ).val() ? $( 'input.qty' ).val() : '1',
+ };
+ ga( 'ec:addProduct', productDetails );
+ ga( 'ec:setAction', 'add' );
+ ga( 'send', 'event', 'UX', 'click', 'add to cart' );
+ } );"
+ );
+ }
+
+ public function loop_add_to_cart() {
+ if ( ! Jetpack_Google_Analytics_Options::track_add_to_cart_is_enabled() ) {
+ return;
+ }
+
+ if ( ! class_exists( 'WooCommerce' ) ) {
+ return;
+ }
+
+ $minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( WC_VERSION, '3.0', '>=' );
+ if ( ! $minimum_woocommerce_active ) {
+ return;
+ }
+
+ $selector = ".add_to_cart_button:not(.product_type_variable, .product_type_grouped)";
+
+ wc_enqueue_js(
+ "$( '" . esc_js( $selector ) . "' ).click( function() {
+ var productSku = $( this ).data( 'product_sku' );
+ var productID = $( this ).data( 'product_id' );
+ var productDetails = {
+ 'id': productSku ? productSku : '#' + productID,
+ 'quantity': $( this ).data( 'quantity' ),
+ };
+ ga( 'ec:addProduct', productDetails );
+ ga( 'ec:setAction', 'add' );
+ ga( 'send', 'event', 'UX', 'click', 'add to cart' );
+ } );"
+ );
+ }
+
+ public function remove_from_cart() {
+ if ( ! Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
+ return;
+ }
+
+ if ( ! Jetpack_Google_Analytics_Options::track_remove_from_cart_is_enabled() ) {
+ return;
+ }
+
+ // We listen at div.woocommerce because the cart 'form' contents get forcibly
+ // updated and subsequent removals from cart would then not have this click
+ // handler attached
+ wc_enqueue_js(
+ "$( 'div.woocommerce' ).on( 'click', 'a.remove', function() {
+ var productSku = $( this ).data( 'product_sku' );
+ var productID = $( this ).data( 'product_id' );
+ var quantity = $( this ).parent().parent().find( '.qty' ).val()
+ var productDetails = {
+ 'id': productSku ? productSku : '#' + productID,
+ 'quantity': quantity ? quantity : '1',
+ };
+ ga( 'ec:addProduct', productDetails );
+ ga( 'ec:setAction', 'remove' );
+ ga( 'send', 'event', 'UX', 'click', 'remove from cart' );
+ } );"
+ );
+ }
+
+ /**
+ * Adds the product ID and SKU to the remove product link (for use by remove_from_cart above) if not present
+ *
+ * @param string $url Full HTML a tag of the link to remove an item from the cart.
+ * @param string $key Unique Key ID for a cart item.
+ */
+ public function remove_from_cart_attributes( $url, $key ) {
+ if ( false !== strpos( $url, 'data-product_id' ) ) {
+ return $url;
+ }
+
+ $item = WC()->cart->get_cart_item( $key );
+ $product = $item['data'];
+
+ $new_attributes = sprintf(
+ '" data-product_id="%1$s" data-product_sku="%2$s">',
+ esc_attr( $product->get_id() ),
+ esc_attr( $product->get_sku() )
+ );
+
+ $url = str_replace( '">', $new_attributes, $url );
+ return $url;
+ }
+
+ public function listing_impression() {
+ if ( ! Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
+ return;
+ }
+
+ if ( ! Jetpack_Google_Analytics_Options::track_product_impressions_is_enabled() ) {
+ return;
+ }
+
+ if ( isset( $_GET['s'] ) ) {
+ $list = "Search Results";
+ } else {
+ $list = "Product List";
+ }
+
+ global $product, $woocommerce_loop;
+ $product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
+
+ $item_details = array(
+ 'id' => $product_sku_or_id,
+ 'name' => $product->get_title(),
+ 'category' => Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
+ 'list' => $list,
+ 'position' => $woocommerce_loop['loop']
+ );
+ wc_enqueue_js( "ga( 'ec:addImpression', " . wp_json_encode( $item_details ) . " );" );
+ }
+
+ public function listing_click() {
+ if ( ! Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
+ return;
+ }
+
+ if ( ! Jetpack_Google_Analytics_Options::track_product_clicks_is_enabled() ) {
+ return;
+ }
+
+ if ( isset( $_GET['s'] ) ) {
+ $list = "Search Results";
+ } else {
+ $list = "Product List";
+ }
+
+ global $product, $woocommerce_loop;
+ $product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
+
+ $selector = ".products .post-" . esc_js( $product->get_id() ) . " a";
+
+ $item_details = array(
+ 'id' => $product_sku_or_id,
+ 'name' => $product->get_title(),
+ 'category' => Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
+ 'position' => $woocommerce_loop['loop']
+ );
+
+ wc_enqueue_js(
+ "$( '" . esc_js( $selector ) . "' ).click( function() {
+ if ( true === $( this ).hasClass( 'add_to_cart_button' ) ) {
+ return;
+ }
+
+ ga( 'ec:addProduct', " . wp_json_encode( $item_details ) . " );
+ ga( 'ec:setAction', 'click', { list: '" . esc_js( $list ) . "' } );
+ ga( 'send', 'event', 'UX', 'click', { list: '" . esc_js( $list ) . "' } );
+ } );"
+ );
+ }
+
+ public function product_detail() {
+ if ( ! Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
+ return;
+ }
+
+ if ( ! Jetpack_Google_Analytics_Options::track_product_detail_view_is_enabled() ) {
+ return;
+ }
+
+ global $product;
+ $product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
+
+ $item_details = array(
+ 'id' => $product_sku_or_id,
+ 'name' => $product->get_title(),
+ 'category' => Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
+ 'price' => $product->get_price()
+ );
+ wc_enqueue_js(
+ "ga( 'ec:addProduct', " . wp_json_encode( $item_details ) . " );" .
+ "ga( 'ec:setAction', 'detail' );"
+ );
+ }
+
+ public function checkout_process() {
+ if ( ! Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
+ return;
+ }
+
+ if ( ! Jetpack_Google_Analytics_Options::track_checkout_started_is_enabled() ) {
+ return;
+ }
+
+ $universal_commands = array();
+ $cart = WC()->cart->get_cart();
+
+ foreach ( $cart as $cart_item_key => $cart_item ) {
+ /**
+ * This filter is already documented in woocommerce/templates/cart/cart.php
+ */
+ $product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
+ $product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
+
+ $item_details = array(
+ 'id' => $product_sku_or_id,
+ 'name' => $product->get_title(),
+ 'category' => Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
+ 'price' => $product->get_price(),
+ 'quantity' => $cart_item[ 'quantity' ]
+ );
+
+ array_push( $universal_commands, "ga( 'ec:addProduct', " . wp_json_encode( $item_details ) . " );" );
+ }
+
+ array_push( $universal_commands, "ga( 'ec:setAction','checkout' );" );
+
+ wc_enqueue_js( implode( "\r\n", $universal_commands ) );
+ }
+
+ public function send_pageview_in_footer() {
+ if ( ! Jetpack_Google_Analytics_Options::has_tracking_code() ) {
+ return;
+ }
+
+ if ( is_admin() ) {
+ return;
+ }
+
+ if ( ! class_exists( 'WooCommerce' ) ) {
+ return;
+ }
+
+ wc_enqueue_js( "ga( 'send', 'pageview' );" );
+ }
+}
diff --git a/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-utils.php b/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-utils.php
new file mode 100644
index 00000000..807461de
--- /dev/null
+++ b/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-utils.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+* Jetpack_Google_Analytics_Options provides a single interface to module options
+*
+* @author allendav
+*/
+
+/**
+* Bail if accessed directly
+*/
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+class Jetpack_Google_Analytics_Utils {
+ /**
+ * Gets product categories or varation attributes as a formatted concatenated string
+ * @param WC_Product
+ * @return string
+ */
+ public static function get_product_categories_concatenated( $product ) {
+ if ( ! class_exists( 'WooCommerce' ) ) {
+ return '';
+ }
+
+ if ( ! $product ) {
+ return '';
+ }
+
+ $variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : '';
+ if ( is_array( $variation_data ) && ! empty( $variation_data ) ) {
+ $line = wc_get_formatted_variation( $variation_data, true );
+ } else {
+ $out = array();
+ $categories = get_the_terms( $product->get_id(), 'product_cat' );
+ if ( $categories ) {
+ foreach ( $categories as $category ) {
+ $out[] = $category->name;
+ }
+ }
+ $line = join( "/", $out );
+ }
+ return $line;
+ }
+
+ /**
+ * Gets a product's SKU with fallback to just ID. IDs are prepended with a hash symbol.
+ * @param WC_Product
+ * @return string
+ */
+ public static function get_product_sku_or_id( $product ) {
+ if ( ! class_exists( 'WooCommerce' ) ) {
+ return '';
+ }
+
+ if ( ! $product ) {
+ return '';
+ }
+
+ return $product->get_sku() ? $product->get_sku() : '#' . $product->get_id();
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/google-analytics/wp-google-analytics.php b/plugins/jetpack/modules/google-analytics/wp-google-analytics.php
new file mode 100644
index 00000000..8d7cdf24
--- /dev/null
+++ b/plugins/jetpack/modules/google-analytics/wp-google-analytics.php
@@ -0,0 +1,76 @@
+<?php
+/*
+ Copyright 2006 Aaron D. Campbell (email : wp_plugins@xavisys.com)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+*/
+
+/**
+ * Jetpack_Google_Analytics is the class that handles ALL of the plugin functionality.
+ * It helps us avoid name collisions
+ * http://codex.wordpress.org/Writing_a_Plugin#Avoiding_Function_Name_Collisions
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+require_once( plugin_basename( 'classes/wp-google-analytics-utils.php' ) );
+require_once( plugin_basename( 'classes/wp-google-analytics-options.php' ) );
+require_once( plugin_basename( 'classes/wp-google-analytics-legacy.php' ) );
+require_once( plugin_basename( 'classes/wp-google-analytics-universal.php' ) );
+
+class Jetpack_Google_Analytics {
+
+ /**
+ * @var Jetpack_Google_Analytics - Static property to hold our singleton instance
+ */
+ static $instance = false;
+
+ /**
+ * @var Static property to hold concrete analytics impl that does the work (universal or legacy)
+ */
+ static $analytics = false;
+
+ /**
+ * This is our constructor, which is private to force the use of get_instance()
+ *
+ * @return void
+ */
+ private function __construct() {
+ // At this time, we only leverage universal analytics when enhanced ecommerce is selected and WooCommerce is active.
+ // Otherwise, don't bother emitting the tracking ID or fetching analytics.js
+ if ( class_exists( 'WooCommerce' ) && Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
+ $analytics = new Jetpack_Google_Analytics_Universal();
+ } else {
+ $analytics = new Jetpack_Google_Analytics_Legacy();
+ }
+
+ }
+
+ /**
+ * Function to instantiate our class and make it a singleton
+ */
+ public static function get_instance() {
+ if ( ! self::$instance ) {
+ self::$instance = new self;
+ }
+
+ return self::$instance;
+ }
+}
+
+global $jetpack_google_analytics;
+$jetpack_google_analytics = Jetpack_Google_Analytics::get_instance();
diff --git a/plugins/jetpack/modules/gplus-authorship.php b/plugins/jetpack/modules/gplus-authorship.php
new file mode 100644
index 00000000..88c13090
--- /dev/null
+++ b/plugins/jetpack/modules/gplus-authorship.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer needed.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/gravatar-hovercards.php b/plugins/jetpack/modules/gravatar-hovercards.php
new file mode 100644
index 00000000..f3deea00
--- /dev/null
+++ b/plugins/jetpack/modules/gravatar-hovercards.php
@@ -0,0 +1,300 @@
+<?php
+/**
+ * Module Name: Gravatar Hovercards
+ * Module Description: Enable pop-up business cards over commenters’ Gravatars.
+ * Sort Order: 11
+ * Recommendation Order: 13
+ * First Introduced: 1.1
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Social, Appearance
+ * Feature: Appearance
+ * Additional Search Queries: gravatar, hovercards
+ */
+
+define( 'GROFILES__CACHE_BUSTER', gmdate( 'YM' ) . 'aa' ); // Break CDN cache, increment when gravatar.com/js/gprofiles.js changes
+
+function grofiles_hovercards_init() {
+ add_filter( 'get_avatar', 'grofiles_get_avatar', 10, 2 );
+ add_action( 'wp_enqueue_scripts', 'grofiles_attach_cards' );
+ add_action( 'wp_footer', 'grofiles_extra_data' );
+ add_action( 'admin_init', 'grofiles_add_settings' );
+
+ add_action( 'load-index.php', 'grofiles_admin_cards' );
+ add_action( 'load-users.php', 'grofiles_admin_cards' );
+ add_action( 'load-edit-comments.php', 'grofiles_admin_cards' );
+ add_action( 'load-options-discussion.php', 'grofiles_admin_cards_forced' );
+
+ add_filter( 'jetpack_module_configuration_url_gravatar-hovercards', 'gravatar_hovercards_configuration_url' );
+}
+
+function gravatar_hovercards_configuration_url() {
+ return admin_url( 'options-discussion.php#show_avatars' );
+}
+
+add_action( 'jetpack_modules_loaded', 'grofiles_hovercards_init' );
+
+/* Hovercard Settings */
+
+/**
+ * Adds Gravatar Hovercard setting
+ *
+ * @todo - always print HTML, hide via CSS/JS if !show_avatars
+ */
+function grofiles_add_settings() {
+ if ( !get_option( 'show_avatars' ) )
+ return;
+
+ add_settings_field( 'gravatar_disable_hovercards', __( 'Gravatar Hovercards', 'jetpack' ), 'grofiles_setting_callback', 'discussion', 'avatars' );
+ register_setting( 'discussion', 'gravatar_disable_hovercards', 'grofiles_hovercard_option_sanitize' );
+}
+
+/**
+ * HTML for Gravatar Hovercard setting
+ */
+function grofiles_setting_callback() {
+ global $current_user;
+
+ $checked = 'disabled' == get_option( 'gravatar_disable_hovercards' ) ? '' : 'checked="checked" ';
+
+ echo "<label id='gravatar-hovercard-options'><input {$checked}name='gravatar_disable_hovercards' id='gravatar_disable_hovercards' type='checkbox' value='enabled' class='code' /> " . __( "View people's profiles when you mouse over their Gravatars", 'jetpack' ) . "</label>";
+?>
+<style type="text/css">
+#grav-profile-example img {
+ float: left;
+}
+#grav-profile-example span {
+ padding: 0 1em;
+}
+</style>
+<script type="text/javascript">
+// <![CDATA[
+jQuery( function($) {
+ var tr = $( '#gravatar_disable_hovercards' ).change( function() {
+ if ( $( this ).is( ':checked' ) ) {
+ $( '#grav-profile-example' ).slideDown( 'fast' );
+ } else {
+ $( '#grav-profile-example' ).slideUp( 'fast' );
+ }
+ } ).parents( 'tr' );
+ var ftr = tr.parents( 'table' ).find( 'tr:first' );
+ if ( ftr.length && !ftr.find( '#gravatar_disable_hovercards' ).length ) {
+ ftr.after( tr );
+ }
+} );
+// ]]>
+</script>
+ <p id="grav-profile-example" class="hide-if-no-js"<?php if ( !$checked ) echo ' style="display:none"'; ?>><?php echo get_avatar( $current_user->ID, 64 ); ?> <span><?php _e( 'Put your mouse over your Gravatar to check out your profile.', 'jetpack' ); ?> <br class="clear" /></span></p>
+<?php
+}
+
+/**
+ * Sanitation filter for Gravatar Hovercard setting
+ */
+function grofiles_hovercard_option_sanitize( $val ) {
+ if ( 'disabled' == $val ) {
+ return $val;
+ }
+
+ return $val ? 'enabled' : 'disabled';
+}
+
+
+/* Hovercard Display */
+
+/**
+ * Stores the gravatars' users that need extra profile data attached.
+ *
+ * Getter/Setter
+ *
+ * @param int|string|null $author Setter: User ID or email address. Getter: null.
+ *
+ * @return mixed Setter: void. Getter: array of user IDs and email addresses.
+ */
+function grofiles_gravatars_to_append( $author = null ) {
+ static $authors = array();
+
+ // Get
+ if ( is_null( $author ) ) {
+ return array_keys( $authors );
+ }
+
+ // Set
+
+ if ( is_numeric( $author ) ) {
+ $author = (int) $author;
+ }
+
+ $authors[$author] = true;
+}
+
+/**
+ * Stores the user ID or email address for each gravatar generated.
+ *
+ * Attached to the 'get_avatar' filter.
+ *
+ * @param string $avatar The <img/> element of the avatar.
+ * @param mixed $author User ID, email address, user login, comment object, user object, post object
+ *
+ * @return The <img/> element of the avatar.
+ */
+function grofiles_get_avatar( $avatar, $author ) {
+ if ( is_numeric( $author ) ) {
+ grofiles_gravatars_to_append( $author );
+ } else if ( is_string( $author ) ) {
+ if ( false !== strpos( $author, '@' ) ) {
+ grofiles_gravatars_to_append( $author );
+ } else {
+ if ( $user = get_user_by( 'slug', $author ) )
+ grofiles_gravatars_to_append( $user->ID );
+ }
+ } else if ( isset( $author->comment_type ) ) {
+ if ( '' != $author->comment_type && 'comment' != $author->comment_type )
+ return $avatar;
+ if ( $author->user_id )
+ grofiles_gravatars_to_append( $author->user_id );
+ else
+ grofiles_gravatars_to_append( $author->comment_author_email );
+ } else if ( isset( $author->user_login ) ) {
+ grofiles_gravatars_to_append( $author->ID );
+ } else if ( isset( $author->post_author ) ) {
+ grofiles_gravatars_to_append( $author->post_author );
+ }
+
+ return $avatar;
+}
+
+/**
+ * Loads Gravatar Hovercard script.
+ *
+ * @todo is_singular() only?
+ */
+function grofiles_attach_cards() {
+ global $blog_id;
+
+ // Is the display of Avatars disabled?
+ if ( ! get_option( 'show_avatars' ) ) {
+ return;
+ }
+
+ // Is the display of Gravatar Hovercards disabled?
+ if ( 'disabled' == Jetpack_Options::get_option_and_ensure_autoload( 'gravatar_disable_hovercards', '0' ) ) {
+ return;
+ }
+
+ wp_enqueue_script( 'grofiles-cards', 'https://secure.gravatar.com/js/gprofiles.js', array( 'jquery' ), GROFILES__CACHE_BUSTER, true );
+ wp_enqueue_script( 'wpgroho', plugins_url( 'wpgroho.js', __FILE__ ), array( 'grofiles-cards' ), false, true );
+ if ( is_user_logged_in() ) {
+ $cu = wp_get_current_user();
+ $my_hash = md5( $cu->user_email );
+ } else if ( !empty( $_COOKIE['comment_author_email_' . COOKIEHASH] ) ) {
+ $my_hash = md5( $_COOKIE['comment_author_email_' . COOKIEHASH] );
+ } else {
+ $my_hash = '';
+ }
+ wp_localize_script( 'wpgroho', 'WPGroHo', compact( 'my_hash' ) );
+}
+
+function grofiles_attach_cards_forced() {
+ add_filter( 'pre_option_gravatar_disable_hovercards', 'grofiles_force_gravatar_enable_hovercards' );
+ grofiles_attach_cards();
+}
+
+function grofiles_force_gravatar_enable_hovercards() {
+ return 'enabled';
+}
+
+function grofiles_admin_cards_forced() {
+ add_action( 'admin_footer', 'grofiles_attach_cards_forced' );
+}
+
+function grofiles_admin_cards() {
+ add_action( 'admin_footer', 'grofiles_attach_cards' );
+}
+
+function grofiles_extra_data() {
+?>
+ <div style="display:none">
+<?php
+ foreach ( grofiles_gravatars_to_append() as $author )
+ grofiles_hovercards_data_html( $author );
+?>
+ </div>
+<?php
+}
+
+/**
+ * Echoes the data from grofiles_hovercards_data() as HTML elements.
+ *
+ * @since 5.5.0 Add support for a passed WP_User object
+ *
+ * @param int|string|WP_User $author User ID, email address, or a WP_User object
+ */
+function grofiles_hovercards_data_html( $author ) {
+ $data = grofiles_hovercards_data( $author );
+ $hash = '';
+ if ( is_numeric( $author ) ) {
+ $user = get_userdata( $author );
+ if ( $user ) {
+ $hash = md5( $user->user_email );
+ }
+ } elseif ( is_email( $author ) ) {
+ $hash = md5( $author );
+ } elseif ( is_a( $author, 'WP_User' ) ) {
+ $hash = md5( $author->user_email );
+ }
+
+ if ( ! $hash ) {
+ return;
+ }
+?>
+ <div class="grofile-hash-map-<?php echo $hash; ?>">
+<?php foreach ( $data as $key => $value ) : ?>
+ <span class="<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $value ); ?></span>
+<?php endforeach; ?>
+ </div>
+<?php
+}
+
+
+/* API */
+
+/**
+ * Returns the PHP callbacks for data sources.
+ *
+ * 'grofiles_hovercards_data_callbacks' filter
+ *
+ * @return array( data_key => data_callback, ... )
+ */
+function grofiles_hovercards_data_callbacks() {
+ /**
+ * Filter the Gravatar Hovercard PHP callbacks.
+ *
+ * @module gravatar-hovercards
+ *
+ * @since 1.1.0
+ *
+ * @param array $args Array of data callbacks.
+ */
+ return apply_filters( 'grofiles_hovercards_data_callbacks', array() );
+}
+
+/**
+ * Keyed JSON object containing all profile data provided by registered callbacks
+ *
+ * @param int|strung $author User ID or email address
+ *
+ * @return array( data_key => data, ... )
+ */
+function grofiles_hovercards_data( $author ) {
+ $r = array();
+ foreach ( grofiles_hovercards_data_callbacks() as $key => $callback ) {
+ if ( !is_callable( $callback ) )
+ continue;
+ $data = call_user_func( $callback, $author, $key );
+ if ( !is_null( $data ) )
+ $r[$key] = $data;
+ }
+
+ return $r;
+}
diff --git a/plugins/jetpack/modules/holiday-snow.php b/plugins/jetpack/modules/holiday-snow.php
new file mode 100644
index 00000000..e8a94faa
--- /dev/null
+++ b/plugins/jetpack/modules/holiday-snow.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer included in Jetpack.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/infinite-scroll.php b/plugins/jetpack/modules/infinite-scroll.php
new file mode 100644
index 00000000..687d2b63
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll.php
@@ -0,0 +1,247 @@
+<?php
+/**
+ * Module Name: Infinite Scroll
+ * Module Description: Automatically load new content when a visitor scrolls
+ * Sort Order: 26
+ * First Introduced: 2.0
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Appearance
+ * Feature: Appearance
+ * Additional Search Queries: scroll, infinite, infinite scroll
+ */
+
+/**
+ * Jetpack-specific elements of Infinite Scroll
+ */
+class Jetpack_Infinite_Scroll_Extras {
+ /**
+ * Class variables
+ */
+ // Oh look, a singleton
+ private static $__instance = null;
+
+ // Option names
+ private $option_name_google_analytics = 'infinite_scroll_google_analytics';
+
+ /**
+ * Singleton implementation
+ *
+ * @return object
+ */
+ public static function instance() {
+ if ( ! is_a( self::$__instance, 'Jetpack_Infinite_Scroll_Extras' ) )
+ self::$__instance = new Jetpack_Infinite_Scroll_Extras;
+
+ return self::$__instance;
+ }
+
+ /**
+ * Register actions and filters
+ *
+ * @uses add_action, add_filter
+ * @return null
+ */
+ private function __construct() {
+ add_action( 'jetpack_modules_loaded', array( $this, 'action_jetpack_modules_loaded' ) );
+
+ add_action( 'admin_init', array( $this, 'action_admin_init' ), 11 );
+
+ add_action( 'after_setup_theme', array( $this, 'action_after_setup_theme' ), 5 );
+
+ add_filter( 'infinite_scroll_js_settings', array( $this, 'filter_infinite_scroll_js_settings' ) );
+
+ add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ) );
+ }
+
+ /**
+ * Enable "Configure" button on module card
+ *
+ * @uses Jetpack::enable_module_configurable
+ * @action jetpack_modules_loaded
+ * @return null
+ */
+ public function action_jetpack_modules_loaded() {
+ Jetpack::enable_module_configurable( __FILE__ );
+ }
+
+ /**
+ * Register Google Analytics setting
+ *
+ * @uses add_settings_field, __, register_setting
+ * @action admin_init
+ * @return null
+ */
+ public function action_admin_init() {
+ add_settings_field( $this->option_name_google_analytics, '<span id="infinite-scroll-google-analytics">' . __( 'Use Google Analytics with Infinite Scroll', 'jetpack' ) . '</span>', array( $this, 'setting_google_analytics' ), 'reading' );
+ register_setting( 'reading', $this->option_name_google_analytics, array( $this, 'sanitize_boolean_value' ) );
+ }
+
+ /**
+ * Render Google Analytics option
+ *
+ * @uses checked, get_option, __
+ * @return html
+ */
+ public function setting_google_analytics() {
+ echo '<label><input name="infinite_scroll_google_analytics" type="checkbox" value="1" ' . checked( true, (bool) get_option( $this->option_name_google_analytics, false ), false ) . ' /> ' . esc_html__( 'Track each scroll load (7 posts by default) as a page view in Google Analytics', 'jetpack' ) . '</label>';
+ echo '<p class="description">' . esc_html__( 'Check the box above to record each new set of posts loaded via Infinite Scroll as a page view in Google Analytics.', 'jetpack' ) . '</p>';
+ }
+
+ /**
+ * Sanitize value as a boolean
+ *
+ * @param mixed $value
+ * @return bool
+ */
+ public function sanitize_boolean_value( $value ) {
+ return (bool) $value;
+ }
+
+ /**
+ * Load theme's infinite scroll annotation file, if present in the IS plugin.
+ * The `setup_theme` action is used because the annotation files should be using `after_setup_theme` to register support for IS.
+ *
+ * As released in Jetpack 2.0, a child theme's parent wasn't checked for in the plugin's bundled support, hence the convoluted way the parent is checked for now.
+ *
+ * @uses is_admin, wp_get_theme, apply_filters
+ * @action setup_theme
+ * @return null
+ */
+ function action_after_setup_theme() {
+ $theme = wp_get_theme();
+
+ if ( ! is_a( $theme, 'WP_Theme' ) && ! is_array( $theme ) )
+ return;
+
+ /** This filter is already documented in modules/infinite-scroll/infinity.php */
+ $customization_file = apply_filters( 'infinite_scroll_customization_file', dirname( __FILE__ ) . "/infinite-scroll/themes/{$theme['Stylesheet']}.php", $theme['Stylesheet'] );
+
+ if ( is_readable( $customization_file ) ) {
+ require_once( $customization_file );
+ }
+ elseif ( ! empty( $theme['Template'] ) ) {
+ $customization_file = dirname( __FILE__ ) . "/infinite-scroll/themes/{$theme['Template']}.php";
+
+ if ( is_readable( $customization_file ) )
+ require_once( $customization_file );
+ }
+ }
+
+ /**
+ * Modify Infinite Scroll configuration information
+ *
+ * @uses Jetpack::get_active_modules, is_user_logged_in, stats_get_options, Jetpack_Options::get_option, get_option, JETPACK__API_VERSION, JETPACK__VERSION
+ * @filter infinite_scroll_js_settings
+ * @return array
+ */
+ public function filter_infinite_scroll_js_settings( $settings ) {
+ // Provide WP Stats info for tracking Infinite Scroll loads
+ // Abort if Stats module isn't active
+ if ( in_array( 'stats', Jetpack::get_active_modules() ) ) {
+ // Abort if user is logged in but logged-in users shouldn't be tracked.
+ if ( is_user_logged_in() && function_exists( 'stats_get_options' ) ) {
+ $stats_options = stats_get_options();
+ $track_loggedin_users = isset( $stats_options['reg_users'] ) ? (bool) $stats_options['reg_users'] : false;
+
+ if ( ! $track_loggedin_users )
+ return $settings;
+ }
+
+ // We made it this far, so gather the data needed to track IS views
+ $settings['stats'] = 'blog=' . Jetpack_Options::get_option( 'id' ) . '&host=' . parse_url( get_option( 'home' ), PHP_URL_HOST ) . '&v=ext&j=' . JETPACK__API_VERSION . ':' . JETPACK__VERSION;
+
+ // Pagetype parameter
+ $settings['stats'] .= '&x_pagetype=infinite';
+ if ( 'click' == $settings['type'] )
+ $settings['stats'] .= '-click';
+
+ $settings['stats'] .= '-jetpack';
+ }
+
+ // Check if Google Analytics tracking is requested
+ $settings['google_analytics'] = (bool) Jetpack_Options::get_option_and_ensure_autoload( $this->option_name_google_analytics, 0 );
+
+ return $settings;
+ }
+
+ /**
+ * Always load certain scripts when IS is enabled, as they can't be loaded after `document.ready` fires, meaning they can't leverage IS's script loader.
+ *
+ * @global $videopress
+ * @uses do_action()
+ * @uses apply_filters()
+ * @uses wp_enqueue_style()
+ * @uses wp_enqueue_script()
+ * @action wp_enqueue_scripts
+ * @return null
+ */
+ public function action_wp_enqueue_scripts() {
+ // Do not load scripts and styles on singular pages and static pages
+ $load_scripts_and_styles = ! ( is_singular() || is_page() );
+ if (
+ /**
+ * Allow plugins to enqueue all Infinite Scroll scripts and styles on singular pages as well.
+ *
+ * @module infinite-scroll
+ *
+ * @since 3.1.0
+ *
+ * @param bool $load_scripts_and_styles Should scripts and styles be loaded on singular pahes and static pages. Default to false.
+ */
+ ! apply_filters( 'jetpack_infinite_scroll_load_scripts_and_styles', $load_scripts_and_styles )
+ ) {
+ return;
+ }
+
+ // VideoPress stand-alone plugin
+ global $videopress;
+ if ( ! empty( $videopress ) && The_Neverending_Home_Page::archive_supports_infinity() && is_a( $videopress, 'VideoPress' ) && method_exists( $videopress, 'enqueue_scripts' ) ) {
+ $videopress->enqueue_scripts();
+ }
+
+ // VideoPress Jetpack module
+ if ( Jetpack::is_module_active( 'videopress' ) ) {
+ wp_enqueue_script( 'videopress' );
+ }
+
+ // Fire the post_gallery action early so Carousel scripts are present.
+ if ( Jetpack::is_module_active( 'carousel' ) ) {
+ /** This filter is already documented in core/wp-includes/media.php */
+ do_action( 'post_gallery', '', '', 0 );
+ }
+
+ // Always enqueue Tiled Gallery scripts when both IS and Tiled Galleries are enabled
+ if ( Jetpack::is_module_active( 'tiled-gallery' ) ) {
+ Jetpack_Tiled_Gallery::default_scripts_and_styles();
+ }
+
+ // Core's Audio and Video Shortcodes
+ if (
+ /** This filter is already documented in core/wp-includes/media.php */
+ 'mediaelement' === apply_filters( 'wp_audio_shortcode_library', 'mediaelement' )
+ ) {
+ wp_enqueue_style( 'wp-mediaelement' );
+ wp_enqueue_script( 'wp-mediaelement' );
+ }
+
+ if (
+ /** This filter is already documented in core/wp-includes/media.php */
+ 'mediaelement' === apply_filters( 'wp_video_shortcode_library', 'mediaelement' )
+ ) {
+ wp_enqueue_style( 'wp-mediaelement' );
+ wp_enqueue_script( 'wp-mediaelement' );
+ }
+ }
+}
+Jetpack_Infinite_Scroll_Extras::instance();
+
+/**
+ * Load main IS file
+ */
+require_once( dirname( __FILE__ ) . "/infinite-scroll/infinity.php" );
+
+/**
+ * Remove the IS annotation loading function bundled with the IS plugin in favor of the Jetpack-specific version in Jetpack_Infinite_Scroll_Extras::action_after_setup_theme();
+ */
+remove_action( 'after_setup_theme', 'the_neverending_home_page_theme_support', 5 );
diff --git a/plugins/jetpack/modules/infinite-scroll/infinity.css b/plugins/jetpack/modules/infinite-scroll/infinity.css
new file mode 100644
index 00000000..4c84e294
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/infinity.css
@@ -0,0 +1,164 @@
+/* =Infinity Styles
+-------------------------------------------------------------- */
+
+.infinite-wrap {
+}
+.infinite-loader {
+ color: #000;
+ display: block;
+ height: 28px;
+ text-indent: -9999px;
+}
+#infinite-handle span {
+ background: #333;
+ border-radius: 1px;
+ color: #eee;
+ cursor: pointer;
+ font-size: 13px;
+ padding: 6px 16px;
+}
+
+/**
+ * Using a highly-specific rule to make sure that all button styles
+ * will be reset
+ */
+#infinite-handle span button,
+#infinite-handle span button:hover,
+#infinite-handle span button:focus {
+ display: inline;
+ position: static;
+ padding: 0;
+ margin: 0;
+ border: none;
+ line-height: inherit;
+ background: transparent;
+ color: inherit;
+ cursor: inherit;
+ font-size: inherit;
+ font-weight: inherit;
+ font-family: inherit;
+}
+
+/**
+ * This is used to avoid unnecessary inner button spacing in Firefox
+ */
+#infinite-handle span button::-moz-focus-inner {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+/**
+ * For smaller viewports, remove the down-arrow icon and turn
+ * the button into a block element, spanning the content's full width.
+ */
+@media (max-width: 800px) {
+ #infinite-handle span:before {
+ display: none;
+ }
+ #infinite-handle span {
+ display: block;
+ }
+}
+
+/**
+ * Footer
+ */
+#infinite-footer {
+ position: fixed;
+ bottom: -50px;
+ left: 0;
+ width: 100%;
+}
+#infinite-footer a {
+ text-decoration: none;
+}
+#infinite-footer .blog-info a:hover,
+#infinite-footer .blog-credits a:hover {
+ color: #444;
+ text-decoration: underline;
+}
+#infinite-footer .container {
+ background: rgba( 255, 255, 255, 0.8 );
+ border-color: #ccc;
+ border-color: rgba( 0, 0, 0, 0.1 );
+ border-style: solid;
+ border-width: 1px 0 0;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ margin: 0 auto;
+ overflow: hidden;
+ padding: 1px 20px;
+ width: 780px;
+}
+#infinite-footer .blog-info,
+#infinite-footer .blog-credits {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ line-height: 25px;
+}
+#infinite-footer .blog-info {
+ float: left;
+ overflow: hidden;
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 40%;
+}
+#infinite-footer .blog-credits {
+ font-weight: normal;
+ float: right;
+ width: 60%;
+}
+#infinite-footer .blog-info a {
+ color: #111;
+ font-size: 14px;
+ font-weight: bold;
+}
+#infinite-footer .blog-credits {
+ color: #888;
+ font-size: 12px;
+ text-align: right;
+}
+#infinite-footer .blog-credits a {
+ color: #666;
+}
+
+/**
+ * Hooks to infinity-end body class to restore footer
+ */
+.infinity-end.neverending #infinite-footer {
+ display: none;
+}
+
+/**
+ * Responsive structure for the footer
+ */
+@media (max-width: 640px) {
+ #infinite-footer .container {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100%;
+ }
+ #infinite-footer .blog-info {
+ width: 30%;
+ }
+ #infinite-footer .blog-credits {
+ width: 70%;
+ }
+ #infinite-footer .blog-info a,
+ #infinite-footer .blog-credits {
+ font-size: 10px;
+ }
+}
+
+/**
+ * No fixed footer on small viewports
+ */
+@media ( max-width: 640px ) {
+ #infinite-footer {
+ position: static;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/infinite-scroll/infinity.js b/plugins/jetpack/modules/infinite-scroll/infinity.js
new file mode 100644
index 00000000..67eed745
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/infinity.js
@@ -0,0 +1,780 @@
+( function( $ ) {
+ // Open closure
+ // Local vars
+ var Scroller, ajaxurl, stats, type, text, totop;
+
+ // IE requires special handling
+ var isIE = -1 != navigator.userAgent.search( 'MSIE' );
+ if ( isIE ) {
+ var IEVersion = navigator.userAgent.match( /MSIE\s?(\d+)\.?\d*;/ );
+ var IEVersion = parseInt( IEVersion[ 1 ] );
+ }
+
+ // HTTP ajaxurl when site is HTTPS causes Access-Control-Allow-Origin failure in Desktop and iOS Safari
+ if ( 'https:' == document.location.protocol ) {
+ infiniteScroll.settings.ajaxurl = infiniteScroll.settings.ajaxurl.replace(
+ 'http://',
+ 'https://'
+ );
+ }
+
+ /**
+ * Loads new posts when users scroll near the bottom of the page.
+ */
+ Scroller = function( settings ) {
+ var self = this;
+
+ // Initialize our variables
+ this.id = settings.id;
+ this.body = $( document.body );
+ this.window = $( window );
+ this.element = $( '#' + settings.id );
+ this.wrapperClass = settings.wrapper_class;
+ this.ready = true;
+ this.disabled = false;
+ this.page = 1;
+ this.offset = settings.offset;
+ this.currentday = settings.currentday;
+ this.order = settings.order;
+ this.throttle = false;
+ this.handle =
+ '<div id="infinite-handle"><span><button>' +
+ text.replace( '\\', '' ) +
+ '</button></span></div>';
+ this.click_handle = settings.click_handle;
+ this.google_analytics = settings.google_analytics;
+ this.history = settings.history;
+ this.origURL = window.location.href;
+ this.pageCache = {};
+
+ // Footer settings
+ this.footer = $( '#infinite-footer' );
+ this.footer.wrap = settings.footer;
+
+ // Core's native MediaElement.js implementation needs special handling
+ this.wpMediaelement = null;
+
+ // We have two type of infinite scroll
+ // cases 'scroll' and 'click'
+
+ if ( type == 'scroll' ) {
+ // Bind refresh to the scroll event
+ // Throttle to check for such case every 300ms
+
+ // On event the case becomes a fact
+ this.window.bind( 'scroll.infinity', function() {
+ this.throttle = true;
+ } );
+
+ // Go back top method
+ self.gotop();
+
+ setInterval( function() {
+ if ( this.throttle ) {
+ // Once the case is the case, the action occurs and the fact is no more
+ this.throttle = false;
+ // Reveal or hide footer
+ self.thefooter();
+ // Fire the refresh
+ self.refresh();
+ self.determineURL(); // determine the url
+ }
+ }, 250 );
+
+ // Ensure that enough posts are loaded to fill the initial viewport, to compensate for short posts and large displays.
+ self.ensureFilledViewport();
+ this.body.bind( 'post-load', { self: self }, self.checkViewportOnLoad );
+ } else if ( type == 'click' ) {
+ if ( this.click_handle ) {
+ this.element.append( this.handle );
+ }
+
+ this.body.delegate( '#infinite-handle', 'click.infinity', function() {
+ // Handle the handle
+ if ( self.click_handle ) {
+ $( '#infinite-handle' ).remove();
+ }
+
+ // Fire the refresh
+ self.refresh();
+ } );
+ }
+
+ // Initialize any Core audio or video players loaded via IS
+ this.body.bind( 'post-load', { self: self }, self.initializeMejs );
+ };
+
+ /**
+ * Check whether we should fetch any additional posts.
+ */
+ Scroller.prototype.check = function() {
+ var container = this.element.offset();
+
+ // If the container can't be found, stop otherwise errors result
+ if ( 'object' !== typeof container ) {
+ return false;
+ }
+
+ var bottom = this.window.scrollTop() + this.window.height(),
+ threshold = container.top + this.element.outerHeight( false ) - this.window.height() * 2;
+
+ return bottom > threshold;
+ };
+
+ /**
+ * Renders the results from a successful response.
+ */
+ Scroller.prototype.render = function( response ) {
+ this.body.addClass( 'infinity-success' );
+
+ // Check if we can wrap the html
+ this.element.append( response.html );
+ this.body.trigger( 'post-load', response );
+ this.ready = true;
+ };
+
+ /**
+ * Returns the object used to query for new posts.
+ */
+ Scroller.prototype.query = function() {
+ return {
+ page: this.page + this.offset, // Load the next page.
+ currentday: this.currentday,
+ order: this.order,
+ scripts: window.infiniteScroll.settings.scripts,
+ styles: window.infiniteScroll.settings.styles,
+ query_args: window.infiniteScroll.settings.query_args,
+ query_before: window.infiniteScroll.settings.query_before,
+ last_post_date: window.infiniteScroll.settings.last_post_date,
+ };
+ };
+
+ /**
+ * Scroll back to top.
+ */
+ Scroller.prototype.gotop = function() {
+ var blog = $( '#infinity-blog-title' );
+
+ blog.attr( 'title', totop );
+
+ // Scroll to top on blog title
+ blog.bind( 'click', function( e ) {
+ $( 'html, body' ).animate( { scrollTop: 0 }, 'fast' );
+ e.preventDefault();
+ } );
+ };
+
+ /**
+ * The infinite footer.
+ */
+ Scroller.prototype.thefooter = function() {
+ var self = this,
+ width;
+
+ // Check if we have an id for the page wrapper
+ if ( $.type( this.footer.wrap ) === 'string' ) {
+ width = $( 'body #' + this.footer.wrap ).outerWidth( false );
+
+ // Make the footer match the width of the page
+ if ( width > 479 ) this.footer.find( '.container' ).css( 'width', width );
+ }
+
+ // Reveal footer
+ if ( this.window.scrollTop() >= 350 ) self.footer.animate( { bottom: 0 }, 'fast' );
+ else if ( this.window.scrollTop() < 350 ) self.footer.animate( { bottom: '-50px' }, 'fast' );
+ };
+
+ /**
+ * Controls the flow of the refresh. Don't mess.
+ */
+ Scroller.prototype.refresh = function() {
+ var self = this,
+ query,
+ jqxhr,
+ load,
+ loader,
+ color,
+ customized;
+
+ // If we're disabled, ready, or don't pass the check, bail.
+ if ( this.disabled || ! this.ready || ! this.check() ) return;
+
+ // Let's get going -- set ready to false to prevent
+ // multiple refreshes from occurring at once.
+ this.ready = false;
+
+ // Create a loader element to show it's working.
+ if ( this.click_handle ) {
+ loader = '<span class="infinite-loader"></span>';
+ this.element.append( loader );
+
+ loader = this.element.find( '.infinite-loader' );
+ color = loader.css( 'color' );
+
+ try {
+ loader.spin( 'medium-left', color );
+ } catch ( error ) {}
+ }
+
+ // Generate our query vars.
+ query = $.extend(
+ {
+ action: 'infinite_scroll',
+ },
+ this.query()
+ );
+
+ // Inject Customizer state.
+ if ( 'undefined' !== typeof wp && wp.customize && wp.customize.settings.theme ) {
+ customized = {};
+ query.wp_customize = 'on';
+ query.theme = wp.customize.settings.theme.stylesheet;
+ wp.customize.each( function( setting ) {
+ if ( setting._dirty ) {
+ customized[ setting.id ] = setting();
+ }
+ } );
+ query.customized = JSON.stringify( customized );
+ query.nonce = wp.customize.settings.nonce.preview;
+ }
+
+ // Fire the ajax request.
+ jqxhr = $.post( infiniteScroll.settings.ajaxurl, query );
+
+ // Allow refreshes to occur again if an error is triggered.
+ jqxhr.fail( function() {
+ if ( self.click_handle ) {
+ loader.hide();
+ }
+
+ self.ready = true;
+ } );
+
+ // Success handler
+ jqxhr.done( function( response ) {
+ // On success, let's hide the loader circle.
+ if ( self.click_handle ) {
+ loader.hide();
+ }
+
+ // Check for and parse our response.
+ if ( ! response || ! response.type ) {
+ return;
+ }
+
+ // If we've succeeded...
+ if ( response.type == 'success' ) {
+ // If additional scripts are required by the incoming set of posts, parse them
+ if ( response.scripts ) {
+ $( response.scripts ).each( function() {
+ var elementToAppendTo = this.footer ? 'body' : 'head';
+
+ // Add script handle to list of those already parsed
+ window.infiniteScroll.settings.scripts.push( this.handle );
+
+ // Output extra data, if present
+ if ( this.extra_data ) {
+ var data = document.createElement( 'script' ),
+ dataContent = document.createTextNode(
+ '//<![CDATA[ \n' + this.extra_data + '\n//]]>'
+ );
+
+ data.type = 'text/javascript';
+ data.appendChild( dataContent );
+
+ document.getElementsByTagName( elementToAppendTo )[ 0 ].appendChild( data );
+ }
+
+ // Build script tag and append to DOM in requested location
+ var script = document.createElement( 'script' );
+ script.type = 'text/javascript';
+ script.src = this.src;
+ script.id = this.handle;
+
+ // If MediaElement.js is loaded in by this set of posts, don't initialize the players a second time as it breaks them all
+ if ( 'wp-mediaelement' === this.handle ) {
+ self.body.unbind( 'post-load', self.initializeMejs );
+ }
+
+ if ( 'wp-mediaelement' === this.handle && 'undefined' === typeof mejs ) {
+ self.wpMediaelement = {};
+ self.wpMediaelement.tag = script;
+ self.wpMediaelement.element = elementToAppendTo;
+ setTimeout( self.maybeLoadMejs.bind( self ), 250 );
+ } else {
+ document.getElementsByTagName( elementToAppendTo )[ 0 ].appendChild( script );
+ }
+ } );
+ }
+
+ // If additional stylesheets are required by the incoming set of posts, parse them
+ if ( response.styles ) {
+ $( response.styles ).each( function() {
+ // Add stylesheet handle to list of those already parsed
+ window.infiniteScroll.settings.styles.push( this.handle );
+
+ // Build link tag
+ var style = document.createElement( 'link' );
+ style.rel = 'stylesheet';
+ style.href = this.src;
+ style.id = this.handle + '-css';
+
+ // Destroy link tag if a conditional statement is present and either the browser isn't IE, or the conditional doesn't evaluate true
+ if (
+ this.conditional &&
+ ( ! isIE || ! eval( this.conditional.replace( /%ver/g, IEVersion ) ) )
+ )
+ var style = false;
+
+ // Append link tag if necessary
+ if ( style ) document.getElementsByTagName( 'head' )[ 0 ].appendChild( style );
+ } );
+ }
+
+ // stash the response in the page cache
+ self.pageCache[ self.page + self.offset ] = response;
+
+ // Increment the page number
+ self.page++;
+
+ // Record pageview in WP Stats, if available.
+ if ( stats )
+ new Image().src =
+ document.location.protocol +
+ '//pixel.wp.com/g.gif?' +
+ stats +
+ '&post=0&baba=' +
+ Math.random();
+
+ // Add new posts to the postflair object
+ if ( 'object' == typeof response.postflair && 'object' == typeof WPCOM_sharing_counts )
+ WPCOM_sharing_counts = $.extend( WPCOM_sharing_counts, response.postflair );
+
+ // Render the results
+ self.render.apply( self, arguments );
+
+ // If 'click' type and there are still posts to fetch, add back the handle
+ if ( type == 'click' ) {
+ if ( response.lastbatch ) {
+ if ( self.click_handle ) {
+ $( '#infinite-handle' ).remove();
+ // Update body classes
+ self.body.addClass( 'infinity-end' ).removeClass( 'infinity-success' );
+ } else {
+ self.body.trigger( 'infinite-scroll-posts-end' );
+ }
+ } else {
+ if ( self.click_handle ) {
+ self.element.append( self.handle );
+ } else {
+ self.body.trigger( 'infinite-scroll-posts-more' );
+ }
+ }
+ } else if ( response.lastbatch ) {
+ self.disabled = true;
+ self.body.addClass( 'infinity-end' ).removeClass( 'infinity-success' );
+ }
+
+ // Update currentday to the latest value returned from the server
+ if ( response.currentday ) {
+ self.currentday = response.currentday;
+ }
+
+ // Fire Google Analytics pageview
+ if ( self.google_analytics ) {
+ var ga_url = self.history.path.replace( /%d/, self.page );
+ if ( 'object' === typeof _gaq ) {
+ _gaq.push( [ '_trackPageview', ga_url ] );
+ }
+ if ( 'function' === typeof ga ) {
+ ga( 'send', 'pageview', ga_url );
+ }
+ }
+ }
+ } );
+
+ return jqxhr;
+ };
+
+ /**
+ * Core's native media player uses MediaElement.js
+ * The library's size is sufficient that it may not be loaded in time for Core's helper to invoke it, so we need to delay until `mejs` exists.
+ */
+ Scroller.prototype.maybeLoadMejs = function() {
+ if ( null === this.wpMediaelement ) {
+ return;
+ }
+
+ if ( 'undefined' === typeof mejs ) {
+ setTimeout( this.maybeLoadMejs, 250 );
+ } else {
+ document
+ .getElementsByTagName( this.wpMediaelement.element )[ 0 ]
+ .appendChild( this.wpMediaelement.tag );
+ this.wpMediaelement = null;
+
+ // Ensure any subsequent IS loads initialize the players
+ this.body.bind( 'post-load', { self: this }, this.initializeMejs );
+ }
+ };
+
+ /**
+ * Initialize the MediaElement.js player for any posts not previously initialized
+ */
+ Scroller.prototype.initializeMejs = function( ev, response ) {
+ // Are there media players in the incoming set of posts?
+ if (
+ ! response.html ||
+ ( -1 === response.html.indexOf( 'wp-audio-shortcode' ) &&
+ -1 === response.html.indexOf( 'wp-video-shortcode' ) )
+ ) {
+ return;
+ }
+
+ // Don't bother if mejs isn't loaded for some reason
+ if ( 'undefined' === typeof mejs ) {
+ return;
+ }
+
+ // Adapted from wp-includes/js/mediaelement/wp-mediaelement.js
+ // Modified to not initialize already-initialized players, as Mejs doesn't handle that well
+ $( function() {
+ var settings = {};
+
+ if ( typeof _wpmejsSettings !== 'undefined' ) {
+ settings.pluginPath = _wpmejsSettings.pluginPath;
+ }
+
+ settings.success = function( mejs ) {
+ var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay;
+ if ( 'flash' === mejs.pluginType && autoplay ) {
+ mejs.addEventListener(
+ 'canplay',
+ function() {
+ mejs.play();
+ },
+ false
+ );
+ }
+ };
+
+ $( '.wp-audio-shortcode, .wp-video-shortcode' )
+ .not( '.mejs-container' )
+ .mediaelementplayer( settings );
+ } );
+ };
+
+ /**
+ * Trigger IS to load additional posts if the initial posts don't fill the window.
+ * On large displays, or when posts are very short, the viewport may not be filled with posts, so we overcome this by loading additional posts when IS initializes.
+ */
+ Scroller.prototype.ensureFilledViewport = function() {
+ var self = this,
+ windowHeight = self.window.height(),
+ postsHeight = self.element.height(),
+ aveSetHeight = 0,
+ wrapperQty = 0;
+
+ // Account for situations where postsHeight is 0 because child list elements are floated
+ if ( postsHeight === 0 ) {
+ $( self.element.selector + ' > li' ).each( function() {
+ postsHeight += $( this ).height();
+ } );
+
+ if ( postsHeight === 0 ) {
+ self.body.unbind( 'post-load', self.checkViewportOnLoad );
+ return;
+ }
+ }
+
+ // Calculate average height of a set of posts to prevent more posts than needed from being loaded.
+ $( '.' + self.wrapperClass ).each( function() {
+ aveSetHeight += $( this ).height();
+ wrapperQty++;
+ } );
+
+ if ( wrapperQty > 0 ) aveSetHeight = aveSetHeight / wrapperQty;
+ else aveSetHeight = 0;
+
+ // Load more posts if space permits, otherwise stop checking for a full viewport
+ if ( postsHeight < windowHeight && postsHeight + aveSetHeight < windowHeight ) {
+ self.ready = true;
+ self.refresh();
+ } else {
+ self.body.unbind( 'post-load', self.checkViewportOnLoad );
+ }
+ };
+
+ /**
+ * Event handler for ensureFilledViewport(), tied to the post-load trigger.
+ * Necessary to ensure that the variable `this` contains the scroller when used in ensureFilledViewport(). Since this function is tied to an event, `this` becomes the DOM element the event is tied to.
+ */
+ Scroller.prototype.checkViewportOnLoad = function( ev ) {
+ ev.data.self.ensureFilledViewport();
+ };
+
+ function fullscreenState() {
+ return document.fullscreenElement ||
+ document.mozFullScreenElement ||
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement
+ ? 1
+ : 0;
+ }
+
+ var previousFullScrenState = fullscreenState();
+
+ /**
+ * Identify archive page that corresponds to majority of posts shown in the current browser window.
+ */
+ Scroller.prototype.determineURL = function() {
+ var self = this,
+ windowTop = $( window ).scrollTop(),
+ windowBottom = windowTop + $( window ).height(),
+ windowSize = windowBottom - windowTop,
+ setsInView = [],
+ setsHidden = [],
+ pageNum = false,
+ currentFullScreenState = fullscreenState();
+
+ // xor - check if the state has changed
+ if ( previousFullScrenState ^ currentFullScreenState ) {
+ // If we just switched to/from fullscreen,
+ // don't do the div clearing/caching or the
+ // URL setting. Doing so can break video playback
+ // if the video goes to fullscreen.
+
+ previousFullScrenState = currentFullScreenState;
+ return;
+ }
+ previousFullScrenState = currentFullScreenState;
+
+ // Find out which sets are in view
+ $( '.' + self.wrapperClass ).each( function() {
+ var id = $( this ).attr( 'id' ),
+ setTop = $( this ).offset().top,
+ setHeight = $( this ).outerHeight( false ),
+ setBottom = 0,
+ setPageNum = $( this ).data( 'page-num' );
+
+ // Account for containers that have no height because their children are floated elements.
+ if ( 0 === setHeight ) {
+ $( '> *', this ).each( function() {
+ setHeight += $( this ).outerHeight( false );
+ } );
+ }
+
+ // Determine position of bottom of set by adding its height to the scroll position of its top.
+ setBottom = setTop + setHeight;
+
+ // Populate setsInView object. While this logic could all be combined into a single conditional statement, this is easier to understand.
+ if ( setTop < windowTop && setBottom > windowBottom ) {
+ // top of set is above window, bottom is below
+ setsInView.push( { id: id, top: setTop, bottom: setBottom, pageNum: setPageNum } );
+ } else if ( setTop > windowTop && setTop < windowBottom ) {
+ // top of set is between top (gt) and bottom (lt)
+ setsInView.push( { id: id, top: setTop, bottom: setBottom, pageNum: setPageNum } );
+ } else if ( setBottom > windowTop && setBottom < windowBottom ) {
+ // bottom of set is between top (gt) and bottom (lt)
+ setsInView.push( { id: id, top: setTop, bottom: setBottom, pageNum: setPageNum } );
+ } else {
+ setsHidden.push( { id: id, top: setTop, bottom: setBottom, pageNum: setPageNum } );
+ }
+ } );
+
+ $.each( setsHidden, function() {
+ var $set = $( '#' + this.id );
+ if ( $set.hasClass( 'is--replaced' ) ) {
+ return;
+ }
+
+ self.pageCache[ this.pageNum ].html = $set.html();
+
+ $set
+ .css( 'min-height', this.bottom - this.top + 'px' )
+ .addClass( 'is--replaced' )
+ .empty();
+ } );
+
+ $.each( setsInView, function() {
+ var $set = $( '#' + this.id );
+
+ if ( $set.hasClass( 'is--replaced' ) ) {
+ $set.css( 'min-height', '' ).removeClass( 'is--replaced' );
+ if ( this.pageNum in self.pageCache ) {
+ $set.html( self.pageCache[ this.pageNum ].html );
+ self.body.trigger( 'post-load', self.pageCache[ this.pageNum ] );
+ }
+ }
+ } );
+
+ // Parse number of sets found in view in an attempt to update the URL to match the set that comprises the majority of the window.
+ if ( 0 == setsInView.length ) {
+ pageNum = -1;
+ } else if ( 1 == setsInView.length ) {
+ var setData = setsInView.pop();
+
+ // If the first set of IS posts is in the same view as the posts loaded in the template by WordPress, determine how much of the view is comprised of IS-loaded posts
+ if ( ( windowBottom - setData.top ) / windowSize < 0.5 ) pageNum = -1;
+ else pageNum = setData.pageNum;
+ } else {
+ var majorityPercentageInView = 0;
+
+ // Identify the IS set that comprises the majority of the current window and set the URL to it.
+ $.each( setsInView, function( i, setData ) {
+ var topInView = 0,
+ bottomInView = 0,
+ percentOfView = 0;
+
+ // Figure percentage of view the current set represents
+ if ( setData.top > windowTop && setData.top < windowBottom )
+ topInView = ( windowBottom - setData.top ) / windowSize;
+
+ if ( setData.bottom > windowTop && setData.bottom < windowBottom )
+ bottomInView = ( setData.bottom - windowTop ) / windowSize;
+
+ // Figure out largest percentage of view for current set
+ if ( topInView >= bottomInView ) percentOfView = topInView;
+ else if ( bottomInView >= topInView ) percentOfView = bottomInView;
+
+ // Does current set's percentage of view supplant the largest previously-found set?
+ if ( percentOfView > majorityPercentageInView ) {
+ pageNum = setData.pageNum;
+ majorityPercentageInView = percentOfView;
+ }
+ } );
+ }
+
+ // If a page number could be determined, update the URL
+ // -1 indicates that the original requested URL should be used.
+ if ( 'number' == typeof pageNum ) {
+ self.updateURL( pageNum );
+ }
+ };
+
+ /**
+ * Update address bar to reflect archive page URL for a given page number.
+ * Checks if URL is different to prevent pollution of browser history.
+ */
+ Scroller.prototype.updateURL = function( page ) {
+ // IE only supports pushState() in v10 and above, so don't bother if those conditions aren't met.
+ if ( ! window.history.pushState ) {
+ return;
+ }
+ var self = this,
+ pageSlug =
+ -1 == page
+ ? self.origURL
+ : window.location.protocol +
+ '//' +
+ self.history.host +
+ self.history.path.replace( /%d/, page ) +
+ self.history.parameters;
+
+ if ( window.location.href != pageSlug ) {
+ history.pushState( null, null, pageSlug );
+ }
+ };
+
+ /**
+ * Pause scrolling.
+ */
+ Scroller.prototype.pause = function() {
+ this.disabled = true;
+ };
+
+ /**
+ * Resume scrolling.
+ */
+ Scroller.prototype.resume = function() {
+ this.disabled = false;
+ };
+
+ /**
+ * Ready, set, go!
+ */
+ $( document ).ready( function() {
+ // Check for our variables
+ if ( 'object' != typeof infiniteScroll ) return;
+
+ $( document.body ).addClass( infiniteScroll.settings.body_class );
+
+ // Set ajaxurl (for brevity)
+ ajaxurl = infiniteScroll.settings.ajaxurl;
+
+ // Set stats, used for tracking stats
+ stats = infiniteScroll.settings.stats;
+
+ // Define what type of infinity we have, grab text for click-handle
+ type = infiniteScroll.settings.type;
+ text = infiniteScroll.settings.text;
+ totop = infiniteScroll.settings.totop;
+
+ // Initialize the scroller (with the ID of the element from the theme)
+ infiniteScroll.scroller = new Scroller( infiniteScroll.settings );
+
+ /**
+ * Monitor user scroll activity to update URL to correspond to archive page for current set of IS posts
+ */
+ if ( type == 'click' ) {
+ var timer = null;
+ $( window ).bind( 'scroll', function() {
+ // run the real scroll handler once every 250 ms.
+ if ( timer ) {
+ return;
+ }
+ timer = setTimeout( function() {
+ infiniteScroll.scroller.determineURL();
+ timer = null;
+ }, 250 );
+ } );
+ }
+
+ // Integrate with Selective Refresh in the Customizer.
+ if ( 'undefined' !== typeof wp && wp.customize && wp.customize.selectiveRefresh ) {
+ /**
+ * Handle rendering of selective refresh partials.
+ *
+ * Make sure that when a partial is rendered, the Jetpack post-load event
+ * will be triggered so that any dynamic elements will be re-constructed,
+ * such as ME.js elements, Photon replacements, social sharing, and more.
+ * Note that this is applying here not strictly to posts being loaded.
+ * If a widget contains a ME.js element and it is previewed via selective
+ * refresh, the post-load would get triggered allowing any dynamic elements
+ * therein to also be re-constructed.
+ *
+ * @param {wp.customize.selectiveRefresh.Placement} placement
+ */
+ wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
+ var content;
+ if ( 'string' === typeof placement.addedContent ) {
+ content = placement.addedContent;
+ } else if ( placement.container ) {
+ content = $( placement.container ).html();
+ }
+
+ if ( content ) {
+ $( document.body ).trigger( 'post-load', { html: content } );
+ }
+ } );
+
+ /*
+ * Add partials for posts added via infinite scroll.
+ *
+ * This is unnecessary when MutationObserver is supported by the browser
+ * since then this will be handled by Selective Refresh in core.
+ */
+ if ( 'undefined' === typeof MutationObserver ) {
+ $( document.body ).on( 'post-load', function( e, response ) {
+ var rootElement = null;
+ if ( response.html && -1 !== response.html.indexOf( 'data-customize-partial' ) ) {
+ if ( infiniteScroll.settings.id ) {
+ rootElement = $( '#' + infiniteScroll.settings.id );
+ }
+ wp.customize.selectiveRefresh.addPartials( rootElement );
+ }
+ } );
+ }
+ }
+ } );
+} )( jQuery ); // Close closure
diff --git a/plugins/jetpack/modules/infinite-scroll/infinity.php b/plugins/jetpack/modules/infinite-scroll/infinity.php
new file mode 100644
index 00000000..35193cfc
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/infinity.php
@@ -0,0 +1,1664 @@
+<?php
+
+/*
+Plugin Name: The Neverending Home Page.
+Plugin URI: http://automattic.com/
+Description: Adds infinite scrolling support to the front-end blog post view for themes, pulling the next set of posts automatically into view when the reader approaches the bottom of the page.
+Version: 1.1
+Author: Automattic
+Author URI: http://automattic.com/
+License: GNU General Public License v2 or later
+License URI: http://www.gnu.org/licenses/gpl-2.0.html
+*/
+
+/**
+ * Class: The_Neverending_Home_Page relies on add_theme_support, expects specific
+ * styling from each theme; including fixed footer.
+ */
+class The_Neverending_Home_Page {
+ /**
+ * Register actions and filters, plus parse IS settings
+ *
+ * @uses add_action, add_filter, self::get_settings
+ * @return null
+ */
+ function __construct() {
+ add_action( 'pre_get_posts', array( $this, 'posts_per_page_query' ) );
+
+ add_action( 'admin_init', array( $this, 'settings_api_init' ) );
+ add_action( 'template_redirect', array( $this, 'action_template_redirect' ) );
+ add_action( 'template_redirect', array( $this, 'ajax_response' ) );
+ add_action( 'custom_ajax_infinite_scroll', array( $this, 'query' ) );
+ add_filter( 'infinite_scroll_query_args', array( $this, 'inject_query_args' ) );
+ add_filter( 'infinite_scroll_allowed_vars', array( $this, 'allowed_query_vars' ) );
+ add_action( 'the_post', array( $this, 'preserve_more_tag' ) );
+ add_action( 'wp_footer', array( $this, 'footer' ) );
+
+ // Plugin compatibility
+ add_filter( 'grunion_contact_form_redirect_url', array( $this, 'filter_grunion_redirect_url' ) );
+
+ // Parse IS settings from theme
+ self::get_settings();
+ }
+
+ /**
+ * Initialize our static variables
+ */
+ static $the_time = null;
+ static $settings = null; // Don't access directly, instead use self::get_settings().
+
+ static $option_name_enabled = 'infinite_scroll';
+
+ /**
+ * Parse IS settings provided by theme
+ *
+ * @uses get_theme_support, infinite_scroll_has_footer_widgets, sanitize_title, add_action, get_option, wp_parse_args, is_active_sidebar
+ * @return object
+ */
+ static function get_settings() {
+ if ( is_null( self::$settings ) ) {
+ $css_pattern = '#[^A-Z\d\-_]#i';
+
+ $settings = $defaults = array(
+ 'type' => 'scroll', // scroll | click
+ 'requested_type' => 'scroll', // store the original type for use when logic overrides it
+ 'footer_widgets' => false, // true | false | sidebar_id | array of sidebar_ids -- last two are checked with is_active_sidebar
+ 'container' => 'content', // container html id
+ 'wrapper' => true, // true | false | html class
+ 'render' => false, // optional function, otherwise the `content` template part will be used
+ 'footer' => true, // boolean to enable or disable the infinite footer | string to provide an html id to derive footer width from
+ 'footer_callback' => false, // function to be called to render the IS footer, in place of the default
+ 'posts_per_page' => false, // int | false to set based on IS type
+ 'click_handle' => true, // boolean to enable or disable rendering the click handler div. If type is click and this is false, page must include its own trigger with the HTML ID `infinite-handle`.
+ );
+
+ // Validate settings passed through add_theme_support()
+ $_settings = get_theme_support( 'infinite-scroll' );
+
+ if ( is_array( $_settings ) ) {
+ // Preferred implementation, where theme provides an array of options
+ if ( isset( $_settings[0] ) && is_array( $_settings[0] ) ) {
+ foreach ( $_settings[0] as $key => $value ) {
+ switch ( $key ) {
+ case 'type' :
+ if ( in_array( $value, array( 'scroll', 'click' ) ) )
+ $settings[ $key ] = $settings['requested_type'] = $value;
+
+ break;
+
+ case 'footer_widgets' :
+ if ( is_string( $value ) )
+ $settings[ $key ] = sanitize_title( $value );
+ elseif ( is_array( $value ) )
+ $settings[ $key ] = array_map( 'sanitize_title', $value );
+ elseif ( is_bool( $value ) )
+ $settings[ $key ] = $value;
+
+ break;
+
+ case 'container' :
+ case 'wrapper' :
+ if ( 'wrapper' == $key && is_bool( $value ) ) {
+ $settings[ $key ] = $value;
+ } else {
+ $value = preg_replace( $css_pattern, '', $value );
+
+ if ( ! empty( $value ) )
+ $settings[ $key ] = $value;
+ }
+
+ break;
+
+ case 'render' :
+ if ( false !== $value && is_callable( $value ) ) {
+ $settings[ $key ] = $value;
+ }
+
+ break;
+
+ case 'footer' :
+ if ( is_bool( $value ) ) {
+ $settings[ $key ] = $value;
+ } elseif ( is_string( $value ) ) {
+ $value = preg_replace( $css_pattern, '', $value );
+
+ if ( ! empty( $value ) )
+ $settings[ $key ] = $value;
+ }
+
+ break;
+
+ case 'footer_callback' :
+ if ( is_callable( $value ) )
+ $settings[ $key ] = $value;
+ else
+ $settings[ $key ] = false;
+
+ break;
+
+ case 'posts_per_page' :
+ if ( is_numeric( $value ) )
+ $settings[ $key ] = (int) $value;
+
+ break;
+
+ case 'click_handle' :
+ if ( is_bool( $value ) ) {
+ $settings[ $key ] = $value;
+ }
+
+ break;
+
+ default:
+ break;
+ }
+ }
+ } elseif ( is_string( $_settings[0] ) ) {
+ // Checks below are for backwards compatibility
+
+ // Container to append new posts to
+ $settings['container'] = preg_replace( $css_pattern, '', $_settings[0] );
+
+ // Wrap IS elements?
+ if ( isset( $_settings[1] ) )
+ $settings['wrapper'] = (bool) $_settings[1];
+ }
+ }
+
+ // Always ensure all values are present in the final array
+ $settings = wp_parse_args( $settings, $defaults );
+
+ // Check if a legacy `infinite_scroll_has_footer_widgets()` function is defined and override the footer_widgets parameter's value.
+ // Otherwise, if a widget area ID or array of IDs was provided in the footer_widgets parameter, check if any contains any widgets.
+ // It is safe to use `is_active_sidebar()` before the sidebar is registered as this function doesn't check for a sidebar's existence when determining if it contains any widgets.
+ if ( function_exists( 'infinite_scroll_has_footer_widgets' ) ) {
+ $settings['footer_widgets'] = (bool) infinite_scroll_has_footer_widgets();
+ } elseif ( is_array( $settings['footer_widgets'] ) ) {
+ $sidebar_ids = $settings['footer_widgets'];
+ $settings['footer_widgets'] = false;
+
+ foreach ( $sidebar_ids as $sidebar_id ) {
+ if ( is_active_sidebar( $sidebar_id ) ) {
+ $settings['footer_widgets'] = true;
+ break;
+ }
+ }
+
+ unset( $sidebar_ids );
+ unset( $sidebar_id );
+ } elseif ( is_string( $settings['footer_widgets'] ) ) {
+ $settings['footer_widgets'] = (bool) is_active_sidebar( $settings['footer_widgets'] );
+ }
+
+ /**
+ * Filter Infinite Scroll's `footer_widgets` parameter.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ *
+ * @param bool $settings['footer_widgets'] Does the current theme have Footer Widgets.
+ */
+ $settings['footer_widgets'] = apply_filters( 'infinite_scroll_has_footer_widgets', $settings['footer_widgets'] );
+
+ // Finally, after all of the sidebar checks and filtering, ensure that a boolean value is present, otherwise set to default of `false`.
+ if ( ! is_bool( $settings['footer_widgets'] ) )
+ $settings['footer_widgets'] = false;
+
+ // Ensure that IS is enabled and no footer widgets exist if the IS type isn't already "click".
+ if ( 'click' != $settings['type'] ) {
+ // Check the setting status
+ $disabled = '' === get_option( self::$option_name_enabled ) ? true : false;
+
+ // Footer content or Reading option check
+ if ( $settings['footer_widgets'] || $disabled )
+ $settings['type'] = 'click';
+ }
+
+ // Force display of the click handler and attendant bits when the type isn't `click`
+ if ( 'click' !== $settings['type'] ) {
+ $settings['click_handle'] = true;
+ }
+
+ // Store final settings in a class static to avoid reparsing
+ /**
+ * Filter the array of Infinite Scroll settings.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ *
+ * @param array $settings Array of Infinite Scroll settings.
+ */
+ self::$settings = apply_filters( 'infinite_scroll_settings', $settings );
+ }
+
+ /** This filter is already documented in modules/infinite-scroll/infinity.php */
+ return (object) apply_filters( 'infinite_scroll_settings', self::$settings );
+ }
+
+ /**
+ * Number of posts per page.
+ *
+ * @uses self::wp_query, self::get_settings, apply_filters
+ * @return int
+ */
+ static function posts_per_page() {
+ $posts_per_page = self::get_settings()->posts_per_page ? self::get_settings()->posts_per_page : self::wp_query()->get( 'posts_per_page' );
+
+ // Take JS query into consideration here
+ if ( true === isset( $_REQUEST['query_args']['posts_per_page'] ) ) {
+ $posts_per_page = $_REQUEST['query_args']['posts_per_page'];
+ }
+
+ /**
+ * Filter the number of posts per page.
+ *
+ * @module infinite-scroll
+ *
+ * @since 6.0.0
+ *
+ * @param int $posts_per_page The number of posts to display per page.
+ */
+ return (int) apply_filters( 'infinite_scroll_posts_per_page', $posts_per_page );
+ }
+
+ /**
+ * Retrieve the query used with Infinite Scroll
+ *
+ * @global $wp_the_query
+ * @uses apply_filters
+ * @return object
+ */
+ static function wp_query() {
+ global $wp_the_query;
+ /**
+ * Filter the Infinite Scroll query object.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.2.1
+ *
+ * @param WP_Query $wp_the_query WP Query.
+ */
+ return apply_filters( 'infinite_scroll_query_object', $wp_the_query );
+ }
+
+ /**
+ * Has infinite scroll been triggered?
+ */
+ static function got_infinity() {
+ /**
+ * Filter the parameter used to check if Infinite Scroll has been triggered.
+ *
+ * @module infinite-scroll
+ *
+ * @since 3.9.0
+ *
+ * @param bool isset( $_GET[ 'infinity' ] ) Return true if the "infinity" parameter is set.
+ */
+ return apply_filters( 'infinite_scroll_got_infinity', isset( $_GET[ 'infinity' ] ) );
+ }
+
+ /**
+ * Is this guaranteed to be the last batch of posts?
+ */
+ static function is_last_batch() {
+ /**
+ * Override whether or not this is the last batch for a request
+ *
+ * @module infinite-scroll
+ *
+ * @since 4.8.0
+ *
+ * @param bool|null null Bool if value should be overridden, null to determine from query
+ * @param object self::wp_query() WP_Query object for current request
+ * @param object self::get_settings() Infinite Scroll settings
+ */
+ $override = apply_filters( 'infinite_scroll_is_last_batch', null, self::wp_query(), self::get_settings() );
+ if ( is_bool( $override ) ) {
+ return $override;
+ }
+
+ $entries = (int) self::wp_query()->found_posts;
+ $posts_per_page = self::posts_per_page();
+
+ // This is to cope with an issue in certain themes or setups where posts are returned but found_posts is 0.
+ if ( 0 == $entries ) {
+ return (bool) ( count( self::wp_query()->posts ) < $posts_per_page );
+ }
+ $paged = max( 1, self::wp_query()->get( 'paged' ) );
+
+ // Are there enough posts for more than the first page?
+ if ( $entries <= $posts_per_page ) {
+ return true;
+ }
+
+ // Calculate entries left after a certain number of pages
+ if ( $paged && $paged > 1 ) {
+ $entries -= $posts_per_page * $paged;
+ }
+
+ // Are there some entries left to display?
+ return $entries <= 0;
+ }
+
+ /**
+ * The more tag will be ignored by default if the blog page isn't our homepage.
+ * Let's force the $more global to false.
+ */
+ function preserve_more_tag( $array ) {
+ global $more;
+
+ if ( self::got_infinity() )
+ $more = 0; //0 = show content up to the more tag. Add more link.
+
+ return $array;
+ }
+
+ /**
+ * Add a checkbox field to Settings > Reading
+ * for enabling infinite scroll.
+ *
+ * Only show if the current theme supports infinity.
+ *
+ * @uses current_theme_supports, add_settings_field, __, register_setting
+ * @action admin_init
+ * @return null
+ */
+ function settings_api_init() {
+ if ( ! current_theme_supports( 'infinite-scroll' ) )
+ return;
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ // This setting is no longer configurable in wp-admin on WordPress.com -- leave a pointer
+ add_settings_field( self::$option_name_enabled,
+ '<span id="infinite-scroll-options">' . esc_html__( 'Infinite Scroll Behavior', 'jetpack' ) . '</span>',
+ array( $this, 'infinite_setting_html_calypso_placeholder' ),
+ 'reading'
+ );
+ return;
+ }
+
+ // Add the setting field [infinite_scroll] and place it in Settings > Reading
+ add_settings_field( self::$option_name_enabled, '<span id="infinite-scroll-options">' . esc_html__( 'Infinite Scroll Behavior', 'jetpack' ) . '</span>', array( $this, 'infinite_setting_html' ), 'reading' );
+ register_setting( 'reading', self::$option_name_enabled, 'esc_attr' );
+ }
+
+ function infinite_setting_html_calypso_placeholder() {
+ $details = get_blog_details();
+ echo '<span>' . sprintf(
+ /* translators: Variables are the enclosing link to the settings page */
+ esc_html__( 'This option has moved. You can now manage it %1$shere%2$s.' ),
+ '<a href="' . esc_url( 'https://wordpress.com/settings/writing/' . $details->domain ) . '">',
+ '</a>'
+ ) . '</span>';
+ }
+
+ /**
+ * HTML code to display a checkbox true/false option
+ * for the infinite_scroll setting.
+ */
+ function infinite_setting_html() {
+ $notice = '<em>' . __( 'We&rsquo;ve changed this option to a click-to-scroll version for you since you have footer widgets in Appearance &rarr; Widgets, or your theme uses click-to-scroll as the default behavior.', 'jetpack' ) . '</em>';
+
+ // If the blog has footer widgets, show a notice instead of the checkbox
+ if ( self::get_settings()->footer_widgets || 'click' == self::get_settings()->requested_type ) {
+ echo '<label>' . $notice . '</label>';
+ } else {
+ echo '<label><input name="infinite_scroll" type="checkbox" value="1" ' . checked( 1, '' !== get_option( self::$option_name_enabled ), false ) . ' /> ' . esc_html__( 'Check to load posts as you scroll. Uncheck to show clickable button to load posts', 'jetpack' ) . '</label>';
+ echo '<p class="description">' . esc_html( sprintf( _n( 'Shows %s post on each load.', 'Shows %s posts on each load.', self::posts_per_page(), 'jetpack' ), number_format_i18n( self::posts_per_page() ) ) ) . '</p>';
+ }
+ }
+
+ /**
+ * Does the legwork to determine whether the feature is enabled.
+ *
+ * @uses current_theme_supports, self::archive_supports_infinity, self::get_settings, add_filter, wp_enqueue_script, plugins_url, wp_enqueue_style, add_action
+ * @action template_redirect
+ * @return null
+ */
+ function action_template_redirect() {
+ // Check that we support infinite scroll, and are on the home page.
+ if ( ! current_theme_supports( 'infinite-scroll' ) || ! self::archive_supports_infinity() )
+ return;
+
+ $id = self::get_settings()->container;
+
+ // Check that we have an id.
+ if ( empty( $id ) )
+ return;
+
+ // Add our scripts.
+ wp_register_script(
+ 'the-neverending-homepage',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/infinite-scroll/infinity.min.js',
+ 'modules/infinite-scroll/infinity.js'
+ ),
+ array( 'jquery' ),
+ '4.0.0',
+ true
+ );
+
+ // Add our default styles.
+ wp_register_style( 'the-neverending-homepage', plugins_url( 'infinity.css', __FILE__ ), array(), '20140422' );
+
+ // Make sure there are enough posts for IS
+ if ( self::is_last_batch() ) {
+ return;
+ }
+
+ // Add our scripts.
+ wp_enqueue_script( 'the-neverending-homepage' );
+
+ // Add our default styles.
+ wp_enqueue_style( 'the-neverending-homepage' );
+
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_spinner_scripts' ) );
+
+ add_action( 'wp_footer', array( $this, 'action_wp_footer_settings' ), 2 );
+
+ add_action( 'wp_footer', array( $this, 'action_wp_footer' ), 21 ); // Core prints footer scripts at priority 20, so we just need to be one later than that
+
+ add_filter( 'infinite_scroll_results', array( $this, 'filter_infinite_scroll_results' ), 10, 3 );
+ }
+
+ /**
+ * Enqueue spinner scripts.
+ */
+ function enqueue_spinner_scripts() {
+ wp_enqueue_script( 'jquery.spin' );
+ }
+
+ /**
+ * Returns classes to be added to <body>. If it's enabled, 'infinite-scroll'. If set to continuous scroll, adds 'neverending' too.
+ *
+ * @since 4.7.0 No longer added as a 'body_class' filter but passed to JS environment and added using JS.
+ *
+ * @return string
+ */
+ function body_class() {
+ $classes = '';
+ // Do not add infinity-scroll class if disabled through the Reading page
+ $disabled = '' === get_option( self::$option_name_enabled ) ? true : false;
+ if ( ! $disabled || 'click' == self::get_settings()->type ) {
+ $classes = 'infinite-scroll';
+
+ if ( 'scroll' == self::get_settings()->type )
+ $classes .= ' neverending';
+ }
+
+ return $classes;
+ }
+
+ /**
+ * In case IS is activated on search page, we have to exclude initially loaded posts which match the keyword by title, not the content as they are displayed before content-matching ones
+ *
+ * @uses self::wp_query
+ * @uses self::get_last_post_date
+ * @uses self::has_only_title_matching_posts
+ * @return array
+ */
+ function get_excluded_posts() {
+
+ $excluded_posts = array();
+ //loop through posts returned by wp_query call
+ foreach( self::wp_query()->get_posts() as $post ) {
+
+ $orderby = isset( self::wp_query()->query_vars['orderby'] ) ? self::wp_query()->query_vars['orderby'] : '';
+ $post_date = ( ! empty( $post->post_date ) ? $post->post_date : false );
+ if ( 'modified' === $orderby || false === $post_date ) {
+ $post_date = $post->post_modified;
+ }
+
+ //in case all posts initially displayed match the keyword by title we add em all to excluded posts array
+ //else, we add only posts which are older than last_post_date param as newer are natually excluded by last_post_date condition in the SQL query
+ if ( self::has_only_title_matching_posts() || $post_date <= self::get_last_post_date() ) {
+ array_push( $excluded_posts, $post->ID );
+ }
+ }
+ return $excluded_posts;
+ }
+
+ /**
+ * In case IS is active on search, we have to exclude posts matched by title rather than by post_content in order to prevent dupes on next pages
+ *
+ * @uses self::wp_query
+ * @uses self::get_excluded_posts
+ * @return array
+ */
+ function get_query_vars() {
+
+ $query_vars = self::wp_query()->query_vars;
+ //applies to search page only
+ if ( true === self::wp_query()->is_search() ) {
+ //set post__not_in array in query_vars in case it does not exists
+ if ( false === isset( $query_vars['post__not_in'] ) ) {
+ $query_vars['post__not_in'] = array();
+ }
+ //get excluded posts
+ $excluded = self::get_excluded_posts();
+ //merge them with other post__not_in posts (eg.: sticky posts)
+ $query_vars['post__not_in'] = array_merge( $query_vars['post__not_in'], $excluded );
+ }
+ return $query_vars;
+ }
+
+ /**
+ * This function checks whether all posts returned by initial wp_query match the keyword by title
+ * The code used in this function is borrowed from WP_Query class where it is used to construct like conditions for keywords
+ *
+ * @uses self::wp_query
+ * @return bool
+ */
+ function has_only_title_matching_posts() {
+
+ //apply following logic for search page results only
+ if ( false === self::wp_query()->is_search() ) {
+ return false;
+ }
+
+ //grab the last posts in the stack as if the last one is title-matching the rest is title-matching as well
+ $post = end( self::wp_query()->posts );
+
+ //code inspired by WP_Query class
+ if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', self::wp_query()->get( 's' ), $matches ) ) {
+ $search_terms = self::wp_query()->query_vars['search_terms'];
+ // if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence
+ if ( empty( $search_terms ) || count( $search_terms ) > 9 ) {
+ $search_terms = array( self::wp_query()->get( 's' ) );
+ }
+ } else {
+ $search_terms = array( self::wp_query()->get( 's' ) );
+ }
+
+ //actual testing. As search query combines multiple keywords with AND, it's enough to check if any of the keywords is present in the title
+ $term = current( $search_terms );
+ if ( ! empty( $term ) && false !== strpos( $post->post_title, $term ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Grab the timestamp for the initial query's last post.
+ *
+ * This takes into account the query's 'orderby' parameter and returns
+ * false if the posts are not ordered by date.
+ *
+ * @uses self::got_infinity
+ * @uses self::has_only_title_matching_posts
+ * @uses self::wp_query
+ * @return string 'Y-m-d H:i:s' or false
+ */
+ function get_last_post_date() {
+ if ( self::got_infinity() )
+ return;
+
+ if ( ! self::wp_query()->have_posts() ) {
+ return null;
+ }
+
+ //In case there are only title-matching posts in the initial WP_Query result, we don't want to use the last_post_date param yet
+ if ( true === self::has_only_title_matching_posts() ) {
+ return false;
+ }
+
+ $post = end( self::wp_query()->posts );
+ $orderby = isset( self::wp_query()->query_vars['orderby'] ) ?
+ self::wp_query()->query_vars['orderby'] : '';
+ $post_date = ( ! empty( $post->post_date ) ? $post->post_date : false );
+ switch ( $orderby ) {
+ case 'modified':
+ return $post->post_modified;
+ case 'date':
+ case '':
+ return $post_date;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Returns the appropriate `wp_posts` table field for a given query's
+ * 'orderby' parameter, if applicable.
+ *
+ * @param optional object $query
+ * @uses self::wp_query
+ * @return string or false
+ */
+ function get_query_sort_field( $query = null ) {
+ if ( empty( $query ) )
+ $query = self::wp_query();
+
+ $orderby = isset( $query->query_vars['orderby'] ) ? $query->query_vars['orderby'] : '';
+
+ switch ( $orderby ) {
+ case 'modified':
+ return 'post_modified';
+ case 'date':
+ case '':
+ return 'post_date';
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Create a where clause that will make sure post queries return posts
+ * in the correct order, without duplicates, if a new post is added
+ * and we're sorting by post date.
+ *
+ * @global $wpdb
+ * @param string $where
+ * @param object $query
+ * @uses apply_filters
+ * @filter posts_where
+ * @return string
+ */
+ function query_time_filter( $where, $query ) {
+ if ( self::got_infinity() ) {
+ global $wpdb;
+
+ $sort_field = self::get_query_sort_field( $query );
+
+ if ( 'post_date' !== $sort_field || 'DESC' !== $_REQUEST['query_args']['order'] ) {
+ return $where;
+ }
+
+ $query_before = sanitize_text_field( wp_unslash( $_REQUEST['query_before'] ) );
+
+ if ( empty( $query_before ) ) {
+ return $where;
+ }
+
+ // Construct the date query using our timestamp
+ $clause = $wpdb->prepare( " AND {$wpdb->posts}.post_date <= %s", $query_before );
+
+ /**
+ * Filter Infinite Scroll's SQL date query making sure post queries
+ * will always return results prior to (descending sort)
+ * or before (ascending sort) the last post date.
+ *
+ * @module infinite-scroll
+ *
+ * @param string $clause SQL Date query.
+ * @param object $query Query.
+ * @param string $operator @deprecated Query operator.
+ * @param string $last_post_date @deprecated Last Post Date timestamp.
+ */
+ $operator = 'ASC' === $_REQUEST['query_args']['order'] ? '>' : '<';
+ $last_post_date = sanitize_text_field( wp_unslash( $_REQUEST['last_post_date'] ) );
+ $where .= apply_filters( 'infinite_scroll_posts_where', $clause, $query, $operator, $last_post_date );
+ }
+
+ return $where;
+ }
+
+ /**
+ * Let's overwrite the default post_per_page setting to always display a fixed amount.
+ *
+ * @param object $query
+ * @uses is_admin, self::archive_supports_infinity, self::get_settings
+ * @return null
+ */
+ function posts_per_page_query( $query ) {
+ if ( ! is_admin() && self::archive_supports_infinity() && $query->is_main_query() )
+ $query->set( 'posts_per_page', self::posts_per_page() );
+ }
+
+ /**
+ * Check if the IS output should be wrapped in a div.
+ * Setting value can be a boolean or a string specifying the class applied to the div.
+ *
+ * @uses self::get_settings
+ * @return bool
+ */
+ function has_wrapper() {
+ return (bool) self::get_settings()->wrapper;
+ }
+
+ /**
+ * Returns the Ajax url
+ *
+ * @global $wp
+ * @uses home_url, add_query_arg, apply_filters
+ * @return string
+ */
+ function ajax_url() {
+ $base_url = set_url_scheme( home_url( '/' ) );
+
+ $ajaxurl = add_query_arg( array( 'infinity' => 'scrolling' ), $base_url );
+
+ /**
+ * Filter the Infinite Scroll Ajax URL.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ *
+ * @param string $ajaxurl Infinite Scroll Ajax URL.
+ */
+ return apply_filters( 'infinite_scroll_ajax_url', $ajaxurl );
+ }
+
+ /**
+ * Our own Ajax response, avoiding calling admin-ajax
+ */
+ function ajax_response() {
+ // Only proceed if the url query has a key of "Infinity"
+ if ( ! self::got_infinity() )
+ return false;
+
+ // This should already be defined below, but make sure.
+ if ( ! defined( 'DOING_AJAX' ) ) {
+ define( 'DOING_AJAX', true );
+ }
+
+ @header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) );
+ send_nosniff_header();
+
+ /**
+ * Fires at the end of the Infinite Scroll Ajax response.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ */
+ do_action( 'custom_ajax_infinite_scroll' );
+ die( '0' );
+ }
+
+ /**
+ * Alias for renamed class method.
+ *
+ * Previously, JS settings object was unnecessarily output in the document head.
+ * When the hook was changed, the method name no longer made sense.
+ */
+ function action_wp_head() {
+ $this->action_wp_footer_settings();
+ }
+
+ /**
+ * Prints the relevant infinite scroll settings in JS.
+ *
+ * @global $wp_rewrite
+ * @uses self::get_settings, esc_js, esc_url_raw, self::has_wrapper, __, apply_filters, do_action, self::get_query_vars
+ * @action wp_footer
+ * @return string
+ */
+ function action_wp_footer_settings() {
+ global $wp_rewrite;
+ global $currentday;
+
+ // Default click handle text
+ $click_handle_text = __( 'Older posts', 'jetpack' );
+
+ // If a single CPT is displayed, use its plural name instead of "posts"
+ // Could be empty (posts) or an array of multiple post types.
+ // In the latter two cases cases, the default text is used, leaving the `infinite_scroll_js_settings` filter for further customization.
+ $post_type = self::wp_query()->get( 'post_type' );
+
+ // If it's a taxonomy, try to change the button text.
+ if ( is_tax() ) {
+ // Get current taxonomy slug.
+ $taxonomy_slug = self::wp_query()->get( 'taxonomy' );
+
+ // Get taxonomy settings.
+ $taxonomy = get_taxonomy( $taxonomy_slug );
+
+ // Check if the taxonomy is attached to one post type only and use its plural name.
+ // If not, use "Posts" without confusing the users.
+ if ( count( $taxonomy->object_type ) < 2 ) {
+ $post_type = $taxonomy->object_type[0];
+ }
+ }
+
+ if ( is_string( $post_type ) && ! empty( $post_type ) ) {
+ $post_type = get_post_type_object( $post_type );
+
+ if ( is_object( $post_type ) && ! is_wp_error( $post_type ) ) {
+ if ( isset( $post_type->labels->name ) ) {
+ $cpt_text = $post_type->labels->name;
+ } elseif ( isset( $post_type->label ) ) {
+ $cpt_text = $post_type->label;
+ }
+
+ if ( isset( $cpt_text ) ) {
+ /* translators: %s is the name of a custom post type */
+ $click_handle_text = sprintf( __( 'More %s', 'jetpack' ), $cpt_text );
+ unset( $cpt_text );
+ }
+ }
+ }
+
+ unset( $post_type );
+
+ // Base JS settings
+ $js_settings = array(
+ 'id' => self::get_settings()->container,
+ 'ajaxurl' => esc_url_raw( self::ajax_url() ),
+ 'type' => esc_js( self::get_settings()->type ),
+ 'wrapper' => self::has_wrapper(),
+ 'wrapper_class' => is_string( self::get_settings()->wrapper ) ? esc_js( self::get_settings()->wrapper ) : 'infinite-wrap',
+ 'footer' => is_string( self::get_settings()->footer ) ? esc_js( self::get_settings()->footer ) : self::get_settings()->footer,
+ 'click_handle' => esc_js( self::get_settings()->click_handle ),
+ 'text' => esc_js( $click_handle_text ),
+ 'totop' => esc_js( __( 'Scroll back to top', 'jetpack' ) ),
+ 'currentday' => $currentday,
+ 'order' => 'DESC',
+ 'scripts' => array(),
+ 'styles' => array(),
+ 'google_analytics' => false,
+ 'offset' => max( 1, self::wp_query()->get( 'paged' ) ), // Pass through the current page so we can use that to offset the first load.
+ 'history' => array(
+ 'host' => preg_replace( '#^http(s)?://#i', '', untrailingslashit( esc_url( get_home_url() ) ) ),
+ 'path' => self::get_request_path(),
+ 'use_trailing_slashes' => $wp_rewrite->use_trailing_slashes,
+ 'parameters' => self::get_request_parameters(),
+ ),
+ 'query_args' => self::get_query_vars(),
+ 'query_before' => current_time( 'mysql' ),
+ 'last_post_date' => self::get_last_post_date(),
+ 'body_class' => self::body_class(),
+ );
+
+ // Optional order param
+ if ( isset( $_REQUEST['order'] ) ) {
+ $order = strtoupper( $_REQUEST['order'] );
+
+ if ( in_array( $order, array( 'ASC', 'DESC' ) ) )
+ $js_settings['order'] = $order;
+ }
+
+ /**
+ * Filter the Infinite Scroll JS settings outputted in the head.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ *
+ * @param array $js_settings Infinite Scroll JS settings.
+ */
+ $js_settings = apply_filters( 'infinite_scroll_js_settings', $js_settings );
+
+ /**
+ * Fires before Infinite Scroll outputs inline JavaScript in the head.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ */
+ do_action( 'infinite_scroll_wp_head' );
+
+ ?>
+ <script type="text/javascript">
+ //<![CDATA[
+ var infiniteScroll = JSON.parse( decodeURIComponent( '<?php echo
+ rawurlencode( json_encode( array( 'settings' => $js_settings ) ) );
+ ?>' ) );
+ //]]>
+ </script>
+ <?php
+ }
+
+ /**
+ * Build path data for current request.
+ * Used for Google Analytics and pushState history tracking.
+ *
+ * @global $wp_rewrite
+ * @global $wp
+ * @uses user_trailingslashit, sanitize_text_field, add_query_arg
+ * @return string|bool
+ */
+ private function get_request_path() {
+ global $wp_rewrite;
+
+ if ( $wp_rewrite->using_permalinks() ) {
+ global $wp;
+
+ // If called too early, bail
+ if ( ! isset( $wp->request ) )
+ return false;
+
+ // Determine path for paginated version of current request
+ if ( false != preg_match( '#' . $wp_rewrite->pagination_base . '/\d+/?$#i', $wp->request ) )
+ $path = preg_replace( '#' . $wp_rewrite->pagination_base . '/\d+$#i', $wp_rewrite->pagination_base . '/%d', $wp->request );
+ else
+ $path = $wp->request . '/' . $wp_rewrite->pagination_base . '/%d';
+
+ // Slashes everywhere we need them
+ if ( 0 !== strpos( $path, '/' ) )
+ $path = '/' . $path;
+
+ $path = user_trailingslashit( $path );
+ } else {
+ // Clean up raw $_REQUEST input
+ $path = array_map( 'sanitize_text_field', $_REQUEST );
+ $path = array_filter( $path );
+
+ $path['paged'] = '%d';
+
+ $path = add_query_arg( $path, '/' );
+ }
+
+ return empty( $path ) ? false : $path;
+ }
+
+ /**
+ * Return query string for current request, prefixed with '?'.
+ *
+ * @return string
+ */
+ private function get_request_parameters() {
+ $uri = $_SERVER[ 'REQUEST_URI' ];
+ $uri = preg_replace( '/^[^?]*(\?.*$)/', '$1', $uri, 1, $count );
+ if ( $count != 1 )
+ return '';
+ return $uri;
+ }
+
+ /**
+ * Provide IS with a list of the scripts and stylesheets already present on the page.
+ * Since posts may contain require additional assets that haven't been loaded, this data will be used to track the additional assets.
+ *
+ * @global $wp_scripts, $wp_styles
+ * @action wp_footer
+ * @return string
+ */
+ function action_wp_footer() {
+ global $wp_scripts, $wp_styles;
+
+ $scripts = is_a( $wp_scripts, 'WP_Scripts' ) ? $wp_scripts->done : array();
+ /**
+ * Filter the list of scripts already present on the page.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.1.2
+ *
+ * @param array $scripts Array of scripts present on the page.
+ */
+ $scripts = apply_filters( 'infinite_scroll_existing_scripts', $scripts );
+
+ $styles = is_a( $wp_styles, 'WP_Styles' ) ? $wp_styles->done : array();
+ /**
+ * Filter the list of styles already present on the page.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.1.2
+ *
+ * @param array $styles Array of styles present on the page.
+ */
+ $styles = apply_filters( 'infinite_scroll_existing_stylesheets', $styles );
+
+ ?><script type="text/javascript">
+ jQuery.extend( infiniteScroll.settings.scripts, <?php echo json_encode( $scripts ); ?> );
+ jQuery.extend( infiniteScroll.settings.styles, <?php echo json_encode( $styles ); ?> );
+ </script><?php
+ }
+
+ /**
+ * Identify additional scripts required by the latest set of IS posts and provide the necessary data to the IS response handler.
+ *
+ * @global $wp_scripts
+ * @uses sanitize_text_field, add_query_arg
+ * @filter infinite_scroll_results
+ * @return array
+ */
+ function filter_infinite_scroll_results( $results, $query_args, $wp_query ) {
+ // Don't bother unless there are posts to display
+ if ( 'success' != $results['type'] )
+ return $results;
+
+ // Parse and sanitize the script handles already output
+ $initial_scripts = isset( $_REQUEST['scripts'] ) && is_array( $_REQUEST['scripts'] ) ? array_map( 'sanitize_text_field', $_REQUEST['scripts'] ) : false;
+
+ if ( is_array( $initial_scripts ) ) {
+ global $wp_scripts;
+
+ // Identify new scripts needed by the latest set of IS posts
+ $new_scripts = array_diff( $wp_scripts->done, $initial_scripts );
+
+ // If new scripts are needed, extract relevant data from $wp_scripts
+ if ( ! empty( $new_scripts ) ) {
+ $results['scripts'] = array();
+
+ foreach ( $new_scripts as $handle ) {
+ // Abort if somehow the handle doesn't correspond to a registered script
+ if ( ! isset( $wp_scripts->registered[ $handle ] ) )
+ continue;
+
+ // Provide basic script data
+ $script_data = array(
+ 'handle' => $handle,
+ 'footer' => ( is_array( $wp_scripts->in_footer ) && in_array( $handle, $wp_scripts->in_footer ) ),
+ 'extra_data' => $wp_scripts->print_extra_script( $handle, false )
+ );
+
+ // Base source
+ $src = $wp_scripts->registered[ $handle ]->src;
+
+ // Take base_url into account
+ if ( strpos( $src, 'http' ) !== 0 )
+ $src = $wp_scripts->base_url . $src;
+
+ // Version and additional arguments
+ if ( null === $wp_scripts->registered[ $handle ]->ver )
+ $ver = '';
+ else
+ $ver = $wp_scripts->registered[ $handle ]->ver ? $wp_scripts->registered[ $handle ]->ver : $wp_scripts->default_version;
+
+ if ( isset( $wp_scripts->args[ $handle ] ) )
+ $ver = $ver ? $ver . '&amp;' . $wp_scripts->args[$handle] : $wp_scripts->args[$handle];
+
+ // Full script source with version info
+ $script_data['src'] = add_query_arg( 'ver', $ver, $src );
+
+ // Add script to data that will be returned to IS JS
+ array_push( $results['scripts'], $script_data );
+ }
+ }
+ }
+
+ // Expose additional script data to filters, but only include in final `$results` array if needed.
+ if ( ! isset( $results['scripts'] ) )
+ $results['scripts'] = array();
+
+ /**
+ * Filter the additional scripts required by the latest set of IS posts.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.1.2
+ *
+ * @param array $results['scripts'] Additional scripts required by the latest set of IS posts.
+ * @param array|bool $initial_scripts Set of scripts loaded on each page.
+ * @param array $results Array of Infinite Scroll results.
+ * @param array $query_args Array of Query arguments.
+ * @param WP_Query $wp_query WP Query.
+ */
+ $results['scripts'] = apply_filters(
+ 'infinite_scroll_additional_scripts',
+ $results['scripts'],
+ $initial_scripts,
+ $results,
+ $query_args,
+ $wp_query
+ );
+
+ if ( empty( $results['scripts'] ) )
+ unset( $results['scripts' ] );
+
+ // Parse and sanitize the style handles already output
+ $initial_styles = isset( $_REQUEST['styles'] ) && is_array( $_REQUEST['styles'] ) ? array_map( 'sanitize_text_field', $_REQUEST['styles'] ) : false;
+
+ if ( is_array( $initial_styles ) ) {
+ global $wp_styles;
+
+ // Identify new styles needed by the latest set of IS posts
+ $new_styles = array_diff( $wp_styles->done, $initial_styles );
+
+ // If new styles are needed, extract relevant data from $wp_styles
+ if ( ! empty( $new_styles ) ) {
+ $results['styles'] = array();
+
+ foreach ( $new_styles as $handle ) {
+ // Abort if somehow the handle doesn't correspond to a registered stylesheet
+ if ( ! isset( $wp_styles->registered[ $handle ] ) )
+ continue;
+
+ // Provide basic style data
+ $style_data = array(
+ 'handle' => $handle,
+ 'media' => 'all'
+ );
+
+ // Base source
+ $src = $wp_styles->registered[ $handle ]->src;
+
+ // Take base_url into account
+ if ( strpos( $src, 'http' ) !== 0 )
+ $src = $wp_styles->base_url . $src;
+
+ // Version and additional arguments
+ if ( null === $wp_styles->registered[ $handle ]->ver )
+ $ver = '';
+ else
+ $ver = $wp_styles->registered[ $handle ]->ver ? $wp_styles->registered[ $handle ]->ver : $wp_styles->default_version;
+
+ if ( isset($wp_styles->args[ $handle ] ) )
+ $ver = $ver ? $ver . '&amp;' . $wp_styles->args[$handle] : $wp_styles->args[$handle];
+
+ // Full stylesheet source with version info
+ $style_data['src'] = add_query_arg( 'ver', $ver, $src );
+
+ // Parse stylesheet's conditional comments if present, converting to logic executable in JS
+ if ( isset( $wp_styles->registered[ $handle ]->extra['conditional'] ) && $wp_styles->registered[ $handle ]->extra['conditional'] ) {
+ // First, convert conditional comment operators to standard logical operators. %ver is replaced in JS with the IE version
+ $style_data['conditional'] = str_replace( array(
+ 'lte',
+ 'lt',
+ 'gte',
+ 'gt'
+ ), array(
+ '%ver <=',
+ '%ver <',
+ '%ver >=',
+ '%ver >',
+ ), $wp_styles->registered[ $handle ]->extra['conditional'] );
+
+ // Next, replace any !IE checks. These shouldn't be present since WP's conditional stylesheet implementation doesn't support them, but someone could be _doing_it_wrong().
+ $style_data['conditional'] = preg_replace( '#!\s*IE(\s*\d+){0}#i', '1==2', $style_data['conditional'] );
+
+ // Lastly, remove the IE strings
+ $style_data['conditional'] = str_replace( 'IE', '', $style_data['conditional'] );
+ }
+
+ // Parse requested media context for stylesheet
+ if ( isset( $wp_styles->registered[ $handle ]->args ) )
+ $style_data['media'] = esc_attr( $wp_styles->registered[ $handle ]->args );
+
+ // Add stylesheet to data that will be returned to IS JS
+ array_push( $results['styles'], $style_data );
+ }
+ }
+ }
+
+ // Expose additional stylesheet data to filters, but only include in final `$results` array if needed.
+ if ( ! isset( $results['styles'] ) )
+ $results['styles'] = array();
+
+ /**
+ * Filter the additional styles required by the latest set of IS posts.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.1.2
+ *
+ * @param array $results['styles'] Additional styles required by the latest set of IS posts.
+ * @param array|bool $initial_styles Set of styles loaded on each page.
+ * @param array $results Array of Infinite Scroll results.
+ * @param array $query_args Array of Query arguments.
+ * @param WP_Query $wp_query WP Query.
+ */
+ $results['styles'] = apply_filters(
+ 'infinite_scroll_additional_stylesheets',
+ $results['styles'],
+ $initial_styles,
+ $results,
+ $query_args,
+ $wp_query
+ );
+
+ if ( empty( $results['styles'] ) )
+ unset( $results['styles' ] );
+
+ // Lastly, return the IS results array
+ return $results;
+ }
+
+ /**
+ * Runs the query and returns the results via JSON.
+ * Triggered by an AJAX request.
+ *
+ * @global $wp_query
+ * @global $wp_the_query
+ * @uses current_theme_supports, get_option, self::wp_query, current_user_can, apply_filters, self::get_settings, add_filter, WP_Query, remove_filter, have_posts, wp_head, do_action, add_action, this::render, this::has_wrapper, esc_attr, wp_footer, sharing_register_post_for_share_counts, get_the_id
+ * @return string or null
+ */
+ function query() {
+ if ( ! isset( $_REQUEST['page'] ) || ! current_theme_supports( 'infinite-scroll' ) )
+ die;
+
+ $page = (int) $_REQUEST['page'];
+
+ // Sanitize and set $previousday. Expected format: dd.mm.yy
+ if ( preg_match( '/^\d{2}\.\d{2}\.\d{2}$/', $_REQUEST['currentday'] ) ) {
+ global $previousday;
+ $previousday = $_REQUEST['currentday'];
+ }
+
+ $post_status = array( 'publish' );
+ if ( current_user_can( 'read_private_posts' ) )
+ array_push( $post_status, 'private' );
+
+ $order = in_array( $_REQUEST['order'], array( 'ASC', 'DESC' ) ) ? $_REQUEST['order'] : 'DESC';
+
+ $query_args = array_merge( self::wp_query()->query_vars, array(
+ 'paged' => $page,
+ 'post_status' => $post_status,
+ 'posts_per_page' => self::posts_per_page(),
+ 'order' => $order
+ ) );
+
+ // 4.0 ?s= compatibility, see https://core.trac.wordpress.org/ticket/11330#comment:50
+ if ( empty( $query_args['s'] ) && ! isset( self::wp_query()->query['s'] ) ) {
+ unset( $query_args['s'] );
+ }
+
+ // By default, don't query for a specific page of a paged post object.
+ // This argument can come from merging self::wp_query() into $query_args above.
+ // Since IS is only used on archives, we should always display the first page of any paged content.
+ unset( $query_args['page'] );
+
+ /**
+ * Filter the array of main query arguments.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.1
+ *
+ * @param array $query_args Array of Query arguments.
+ */
+ $query_args = apply_filters( 'infinite_scroll_query_args', $query_args );
+
+ add_filter( 'posts_where', array( $this, 'query_time_filter' ), 10, 2 );
+
+ $GLOBALS['wp_the_query'] = $GLOBALS['wp_query'] = $infinite_scroll_query = new WP_Query();
+
+ $infinite_scroll_query->query( $query_args );
+
+ remove_filter( 'posts_where', array( $this, 'query_time_filter' ), 10, 2 );
+
+ $results = array();
+
+ if ( have_posts() ) {
+ // Fire wp_head to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
+ ob_start();
+ wp_head();
+ while ( ob_get_length() ) {
+ ob_end_clean();
+ }
+
+ $results['type'] = 'success';
+
+ /**
+ * Gather renderer callbacks. These will be called in order and allow multiple callbacks to be queued. Once content is found, no futher callbacks will run.
+ *
+ * @module infinite-scroll
+ *
+ * @since 6.0.0
+ */
+ $callbacks = apply_filters( 'infinite_scroll_render_callbacks', array(
+ self::get_settings()->render, // This is the setting callback e.g. from add theme support.
+ ) );
+
+ // Append fallback callback. That rhymes.
+ $callbacks[] = array( $this, 'render' );
+
+ foreach ( $callbacks as $callback ) {
+ if ( false !== $callback && is_callable( $callback ) ) {
+ rewind_posts();
+ ob_start();
+ add_action( 'infinite_scroll_render', $callback );
+
+ /**
+ * Fires when rendering Infinite Scroll posts.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ */
+ do_action( 'infinite_scroll_render' );
+
+ $results['html'] = ob_get_clean();
+ remove_action( 'infinite_scroll_render', $callback );
+ }
+ if ( ! empty( $results['html'] ) ) {
+ break;
+ }
+ }
+
+ // If primary and fallback rendering methods fail, prevent further IS rendering attempts. Otherwise, wrap the output if requested.
+ if ( empty( $results['html'] ) ) {
+ unset( $results['html'] );
+ /**
+ * Fires when Infinite Scoll doesn't render any posts.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ */
+ do_action( 'infinite_scroll_empty' );
+ $results['type'] = 'empty';
+ } elseif ( $this->has_wrapper() ) {
+ $wrapper_classes = is_string( self::get_settings()->wrapper ) ? self::get_settings()->wrapper : 'infinite-wrap';
+ $wrapper_classes .= ' infinite-view-' . $page;
+ $wrapper_classes = trim( $wrapper_classes );
+
+ $results['html'] = '<div class="' . esc_attr( $wrapper_classes ) . '" id="infinite-view-' . $page . '" data-page-num="' . $page . '">' . $results['html'] . '</div>';
+ }
+
+ // Fire wp_footer to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
+ ob_start();
+ wp_footer();
+ while ( ob_get_length() ) {
+ ob_end_clean();
+ }
+
+ if ( 'success' == $results['type'] ) {
+ global $currentday;
+ $results['lastbatch'] = self::is_last_batch();
+ $results['currentday'] = $currentday;
+ }
+
+ // Loop through posts to capture sharing data for new posts loaded via Infinite Scroll
+ if ( 'success' == $results['type'] && function_exists( 'sharing_register_post_for_share_counts' ) ) {
+ global $jetpack_sharing_counts;
+
+ while( have_posts() ) {
+ the_post();
+
+ sharing_register_post_for_share_counts( get_the_ID() );
+ }
+
+ $results['postflair'] = array_flip( $jetpack_sharing_counts );
+ }
+ } else {
+ /** This action is already documented in modules/infinite-scroll/infinity.php */
+ do_action( 'infinite_scroll_empty' );
+ $results['type'] = 'empty';
+ }
+
+ wp_send_json(
+ /**
+ * Filter the Infinite Scroll results.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ *
+ * @param array $results Array of Infinite Scroll results.
+ * @param array $query_args Array of main query arguments.
+ * @param WP_Query $wp_query WP Query.
+ */
+ apply_filters( 'infinite_scroll_results', $results, $query_args, self::wp_query() )
+ );
+ }
+
+ /**
+ * Update the $allowed_vars array with the standard WP public and private
+ * query vars, as well as taxonomy vars
+ *
+ * @global $wp
+ * @param array $allowed_vars
+ * @filter infinite_scroll_allowed_vars
+ * @return array
+ */
+ function allowed_query_vars( $allowed_vars ) {
+ global $wp;
+
+ $allowed_vars += $wp->public_query_vars;
+ $allowed_vars += $wp->private_query_vars;
+ $allowed_vars += $this->get_taxonomy_vars();
+
+ foreach ( array_keys( $allowed_vars, 'paged' ) as $key ) {
+ unset( $allowed_vars[ $key ] );
+ }
+
+ return array_unique( $allowed_vars );
+ }
+
+ /**
+ * Returns an array of stock and custom taxonomy query vars
+ *
+ * @global $wp_taxonomies
+ * @return array
+ */
+ function get_taxonomy_vars() {
+ global $wp_taxonomies;
+
+ $taxonomy_vars = array();
+ foreach ( $wp_taxonomies as $taxonomy => $t ) {
+ if ( $t->query_var )
+ $taxonomy_vars[] = $t->query_var;
+ }
+
+ // still needed?
+ $taxonomy_vars[] = 'tag_id';
+
+ return $taxonomy_vars;
+ }
+
+ /**
+ * Update the $query_args array with the parameters provided via AJAX/GET.
+ *
+ * @param array $query_args
+ * @filter infinite_scroll_query_args
+ * @return array
+ */
+ function inject_query_args( $query_args ) {
+ /**
+ * Filter the array of allowed Infinite Scroll query arguments.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.6.0
+ *
+ * @param array $args Array of allowed Infinite Scroll query arguments.
+ * @param array $query_args Array of query arguments.
+ */
+ $allowed_vars = apply_filters( 'infinite_scroll_allowed_vars', array(), $query_args );
+
+ $query_args = array_merge( $query_args, array(
+ 'suppress_filters' => false,
+ ) );
+
+ if ( is_array( $_REQUEST[ 'query_args' ] ) ) {
+ foreach ( $_REQUEST[ 'query_args' ] as $var => $value ) {
+ if ( in_array( $var, $allowed_vars ) && ! empty( $value ) )
+ $query_args[ $var ] = $value;
+ }
+ }
+
+ return $query_args;
+ }
+
+ /**
+ * Rendering fallback used when themes don't specify their own handler.
+ *
+ * @uses have_posts, the_post, get_template_part, get_post_format
+ * @action infinite_scroll_render
+ * @return string
+ */
+ function render() {
+ while ( have_posts() ) {
+ the_post();
+
+ get_template_part( 'content', get_post_format() );
+ }
+ }
+
+ /**
+ * Allow plugins to filter what archives Infinite Scroll supports
+ *
+ * @uses current_theme_supports, is_home, is_archive, apply_filters, self::get_settings
+ * @return bool
+ */
+ public static function archive_supports_infinity() {
+ $supported = current_theme_supports( 'infinite-scroll' ) && ( is_home() || is_archive() || is_search() );
+
+ // Disable when previewing a non-active theme in the customizer
+ if ( is_customize_preview() && ! $GLOBALS['wp_customize']->is_theme_active() ) {
+ return false;
+ }
+
+ /**
+ * Allow plugins to filter what archives Infinite Scroll supports.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ *
+ * @param bool $supported Does the Archive page support Infinite Scroll.
+ * @param object self::get_settings() IS settings provided by theme.
+ */
+ return (bool) apply_filters( 'infinite_scroll_archive_supported', $supported, self::get_settings() );
+ }
+
+ /**
+ * The Infinite Blog Footer
+ *
+ * @uses self::get_settings, self::archive_supports_infinity, self::default_footer
+ * @return string or null
+ */
+ function footer() {
+ // Bail if theme requested footer not show
+ if ( false == self::get_settings()->footer )
+ return;
+
+ // We only need the new footer for the 'scroll' type
+ if ( 'scroll' != self::get_settings()->type || ! self::archive_supports_infinity() )
+ return;
+
+ if ( self::is_last_batch() ) {
+ return;
+ }
+
+ // Display a footer, either user-specified or a default
+ if ( false !== self::get_settings()->footer_callback && is_callable( self::get_settings()->footer_callback ) )
+ call_user_func( self::get_settings()->footer_callback, self::get_settings() );
+ else
+ self::default_footer();
+ }
+
+ /**
+ * Render default IS footer
+ *
+ * @uses __, wp_get_theme, apply_filters, home_url, esc_attr, get_bloginfo, bloginfo
+ * @return string
+ *
+ */
+ private function default_footer() {
+ if ( '' !== get_privacy_policy_url() ) {
+ $credits = get_the_privacy_policy_link() . '<span role="separator" aria-hidden="true"> / </span>';
+ } else {
+ $credits = '';
+ }
+ $credits .= sprintf(
+ '<a href="https://wordpress.org/" rel="noopener noreferrer" target="_blank" rel="generator">%1$s</a> ',
+ __( 'Proudly powered by WordPress', 'jetpack' )
+ );
+ $credits .= sprintf(
+ /* translators: %1$s is the name of a theme */
+ __( 'Theme: %1$s.', 'jetpack' ),
+ wp_get_theme()->Name
+ );
+ /**
+ * Filter Infinite Scroll's credit text.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ *
+ * @param string $credits Infinite Scroll credits.
+ */
+ $credits = apply_filters( 'infinite_scroll_credit', $credits );
+
+ ?>
+ <div id="infinite-footer">
+ <div class="container">
+ <div class="blog-info">
+ <a id="infinity-blog-title" href="<?php echo home_url( '/' ); ?>" rel="home">
+ <?php bloginfo( 'name' ); ?>
+ </a>
+ </div>
+ <div class="blog-credits">
+ <?php echo $credits; ?>
+ </div>
+ </div>
+ </div><!-- #infinite-footer -->
+ <?php
+ }
+
+ /**
+ * Ensure that IS doesn't interfere with Grunion by stripping IS query arguments from the Grunion redirect URL.
+ * When arguments are present, Grunion redirects to the IS AJAX endpoint.
+ *
+ * @param string $url
+ * @uses remove_query_arg
+ * @filter grunion_contact_form_redirect_url
+ * @return string
+ */
+ public function filter_grunion_redirect_url( $url ) {
+ // Remove IS query args, if present
+ if ( false !== strpos( $url, 'infinity=scrolling' ) ) {
+ $url = remove_query_arg( array(
+ 'infinity',
+ 'action',
+ 'page',
+ 'order',
+ 'scripts',
+ 'styles'
+ ), $url );
+ }
+
+ return $url;
+ }
+};
+
+/**
+ * Initialize The_Neverending_Home_Page
+ */
+function the_neverending_home_page_init() {
+ if ( ! current_theme_supports( 'infinite-scroll' ) )
+ return;
+
+ new The_Neverending_Home_Page;
+}
+add_action( 'init', 'the_neverending_home_page_init', 20 );
+
+/**
+ * Check whether the current theme is infinite-scroll aware.
+ * If so, include the files which add theme support.
+ */
+function the_neverending_home_page_theme_support() {
+ if (
+ defined( 'IS_WPCOM' ) && IS_WPCOM &&
+ defined( 'REST_API_REQUEST' ) && REST_API_REQUEST &&
+ ! doing_action( 'restapi_theme_after_setup_theme' )
+ ) {
+ // Don't source theme compat files until we're in the site's context
+ return;
+ }
+ $theme_name = get_stylesheet();
+
+ /**
+ * Filter the path to the Infinite Scroll compatibility file.
+ *
+ * @module infinite-scroll
+ *
+ * @since 2.0.0
+ *
+ * @param string $str IS compatibility file path.
+ * @param string $theme_name Theme name.
+ */
+ $customization_file = apply_filters( 'infinite_scroll_customization_file', dirname( __FILE__ ) . "/themes/{$theme_name}.php", $theme_name );
+
+ if ( is_readable( $customization_file ) )
+ require_once( $customization_file );
+}
+add_action( 'after_setup_theme', 'the_neverending_home_page_theme_support', 5 );
+
+/**
+ * Early accommodation of the Infinite Scroll AJAX request
+ */
+if ( The_Neverending_Home_Page::got_infinity() ) {
+ /**
+ * If we're sure this is an AJAX request (i.e. the HTTP_X_REQUESTED_WITH header says so),
+ * indicate it as early as possible for actions like init
+ */
+ if ( ! defined( 'DOING_AJAX' ) &&
+ isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) &&
+ strtoupper( $_SERVER['HTTP_X_REQUESTED_WITH'] ) == 'XMLHTTPREQUEST'
+ ) {
+ define( 'DOING_AJAX', true );
+ }
+
+ // Don't load the admin bar when doing the AJAX response.
+ show_admin_bar( false );
+}
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.css b/plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.css
new file mode 100644
index 00000000..cc232785
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.css
@@ -0,0 +1,45 @@
+/* =Infinity Styles
+-------------------------------------------------------------- */
+.infinite-scroll #main:after {
+ clear: both;
+ content: '';
+ display: block;
+}
+.infinite-scroll #content {
+ margin-bottom: 40px;
+}
+.infinite-scroll.neverending #content {
+ margin-bottom: 70px;
+}
+.infinite-scroll .infinite-wrap {
+ border-top: none;
+ padding-top: 0;
+}
+.infinite-scroll .infinite-wrap .hentry:last-child {
+ border-bottom: 1px solid #ddd;
+}
+.infinite-scroll .infinite-wrap:last-of-type .hentry:last-child {
+ border-bottom: none;
+}
+
+/**
+ * Elements to hide:
+ * (footer widgets, post navigation, regular footer)
+ */
+.infinite-scroll.neverending #colophon #supplementary,
+.infinite-scroll #nav-below,
+.infinite-scroll.neverending #colophon {
+ display: none;
+}
+
+/* Hooks to infinity-end body class to restore footer */
+.infinity-end.neverending #colophon {
+ display: block;
+}
+
+/* For responsive CSS */
+@media (max-width: 800px) {
+ .infinite-scroll #infinite-handle {
+ padding-bottom: 40px;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.php b/plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.php
new file mode 100644
index 00000000..42a69b2d
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Infinite Scroll Theme Assets
+ *
+ * Register support for @Twenty Eleven and enqueue relevant styles.
+ */
+
+/**
+ * Add theme support for infinity scroll
+ */
+function jetpack_twentyeleven_infinite_scroll_init() {
+ add_theme_support( 'infinite-scroll', array(
+ 'container' => 'content',
+ 'footer' => 'page',
+ 'footer_widgets' => jetpack_twentyeleven_has_footer_widgets(),
+ ) );
+}
+add_action( 'init', 'jetpack_twentyeleven_infinite_scroll_init' );
+
+/**
+ * Enqueue CSS stylesheet with theme styles for infinity.
+ */
+function jetpack_twentyeleven_infinite_scroll_enqueue_styles() {
+ if ( wp_script_is( 'the-neverending-homepage' ) ) {
+ // Add theme specific styles.
+ wp_enqueue_style( 'infinity-twentyeleven', plugins_url( 'twentyeleven.css', __FILE__ ), array( 'the-neverending-homepage' ), '20121002' );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'jetpack_twentyeleven_infinite_scroll_enqueue_styles', 25 );
+
+/**
+ * Do we have footer widgets?
+ */
+function jetpack_twentyeleven_has_footer_widgets() {
+ // Are any of the "Footer Area" sidebars active?
+ if ( is_active_sidebar( 'sidebar-3' ) || is_active_sidebar( 'sidebar-4' ) || is_active_sidebar( 'sidebar-5' ) )
+ return true;
+
+ // If we're on mobile and the Main Sidebar has widgets, it falls below the content, so we have footer widgets.
+ if ( function_exists( 'jetpack_is_mobile' ) && jetpack_is_mobile() && is_active_sidebar( 'sidebar-1' ) )
+ return true;
+
+ return false;
+}
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen-rtl.css b/plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen-rtl.css
new file mode 100644
index 00000000..c7bb9595
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen-rtl.css
@@ -0,0 +1,216 @@
+/**
+ * Infinite Scroll
+ */
+
+.infinite-scroll .pagination,
+.infinite-scroll.neverending .site-footer {
+ display: none;
+}
+
+.infinity-end.neverending .site-footer {
+ display: block;
+}
+
+/* Spinner */
+.infinite-loader {
+ clear: both;
+ height: 24px;
+ margin: 24px 0;
+}
+
+.infinite-loader .spinner {
+ top: 50% !important;
+ right: 50% !important;
+}
+
+/* Click-to-load */
+#infinite-handle {
+ clear: both;
+ margin: 7.6923%;
+ text-align: center;
+}
+
+#infinite-handle span {
+ background-color: #333;
+ font-family: "Noto Sans", sans-serif;
+ font-size: 12px;
+ font-size: 1.2rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ line-height: normal;
+ padding: 0.7917em;
+ text-transform: uppercase;
+}
+
+#infinite-handle span:hover,
+#infinite-handle span:focus {
+ background-color: #707070;
+ background-color: rgba(51, 51, 51, 0.7);
+ color: #fff;
+}
+
+/* Footer */
+#infinite-footer {
+ display: none;
+ z-index: 999;
+}
+
+#infinite-footer .container {
+ background-color: #fff;
+ background-color: rgba(255, 255, 255, 0.5);
+ border-color: #eaeaea;
+ border-color: rgba(51, 51, 51, 0.1);
+ padding: 0 0.8em;
+ width: 100% !important;
+}
+
+#infinite-footer .blog-info {
+ font-family: "Noto Sans", sans-serif;
+}
+
+#infinite-footer .blog-info,
+#infinite-footer .blog-credits {
+ height: 24px;
+ line-height: 24px;
+}
+
+#infinite-footer .blog-info a,
+#infinite-footer .blog-credits {
+ font-size: 12px;
+ font-size: 1.2rem;
+}
+
+#infinite-footer .blog-info a,
+#infinite-footer .blog-credits a:hover,
+#infinite-footer .blog-credits a:focus {
+ color: #333;
+}
+
+#infinite-footer .blog-info a:hover,
+#infinite-footer .blog-info a:focus,
+#infinite-footer .blog-credits,
+#infinite-footer .blog-credits a {
+ color: #707070;
+ color: rgba(51, 51, 51, 0.7);
+}
+
+#infinite-footer .blog-info a:hover,
+#infinite-footer .blog-info a:focus,
+#infinite-footer .blog-credits a:hover,
+#infinite-footer .blog-credits a:focus {
+ text-decoration: none;
+}
+
+@media screen and (min-width: 38.75em) {
+ .infinite-loader {
+ margin: 7.6923% 0;
+ }
+
+ .infinite-wrap {
+ margin-top: 7.6923%;
+ }
+
+ #infinite-handle {
+ margin-bottom: 0;
+ }
+}
+
+@media screen and (min-width: 46.25em) {
+ #infinite-handle span {
+ display: block;
+ font-size: 14px;
+ font-size: 1.4rem;
+ padding: 0.8214em
+ }
+}
+
+@media screen and (min-width: 55em) {
+ #infinite-handle span {
+ font-size: 16px;
+ font-size: 1.6rem;
+ padding: 0.8125em;
+ }
+}
+
+@media screen and (min-width: 59.6875em) {
+ .infinite-loader {
+ margin: 8.3333% 0;
+ }
+
+ .infinite-wrap {
+ margin-top: 8.3333%;
+ }
+
+ #infinite-handle {
+ margin: 8.3333% 8.3333% 0;
+ }
+
+ #infinite-handle span {
+ display: inline-block;
+ font-size: 12px;
+ font-size: 1.2rem;
+ padding: 0.7917em 1.5833em;
+ }
+
+ #infinite-footer {
+ display: block;
+ position: fixed;
+ }
+}
+
+@media screen and (min-width: 68.75em) {
+ #infinite-handle span {
+ display: inline-block;
+ font-size: 14px;
+ font-size: 1.4rem;
+ padding: 0.8214em 1.5714em;
+ }
+
+ #infinite-footer .container {
+ padding: 0 0.8235em;
+ }
+
+ #infinite-footer .blog-info,
+ #infinite-footer .blog-credits {
+ height: 27px;
+ line-height: 27px;
+ }
+
+ #infinite-footer .blog-info a {
+ font-size: 14px;
+ font-size: 1.4rem;
+ }
+
+ #infinite-footer .blog-credits {
+ font-size: 12px;
+ font-size: 1.2rem;
+ }
+}
+
+@media screen and (min-width: 77.5em) {
+ #infinite-handle span {
+ font-size: 16px;
+ font-size: 1.6rem;
+ padding: 0.8125em 1.625em;
+ }
+
+ #infinite-footer .container {
+ padding: 0 0.8421em;
+ }
+
+ #infinite-footer .blog-info,
+ #infinite-footer .blog-credits {
+ height: 32px;
+ line-height: 32px;
+ }
+
+ #infinite-footer .blog-info a {
+ font-size: 16px;
+ font-size: 1.6rem;
+ }
+
+ #infinite-footer .blog-credits {
+ font-size: 13px;
+ font-size: 1.3rem;
+ }
+}
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen.css b/plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen.css
new file mode 100644
index 00000000..17ff0983
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen.css
@@ -0,0 +1,216 @@
+/**
+ * Infinite Scroll
+ */
+
+.infinite-scroll .pagination,
+.infinite-scroll.neverending .site-footer {
+ display: none;
+}
+
+.infinity-end.neverending .site-footer {
+ display: block;
+}
+
+/* Spinner */
+.infinite-loader {
+ clear: both;
+ height: 24px;
+ margin: 24px 0;
+}
+
+.infinite-loader .spinner {
+ top: 50% !important;
+ left: 50% !important;
+}
+
+/* Click-to-load */
+#infinite-handle {
+ clear: both;
+ margin: 7.6923%;
+ text-align: center;
+}
+
+#infinite-handle span {
+ background-color: #333;
+ font-family: "Noto Sans", sans-serif;
+ font-size: 12px;
+ font-size: 1.2rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ line-height: normal;
+ padding: 0.7917em;
+ text-transform: uppercase;
+}
+
+#infinite-handle span:hover,
+#infinite-handle span:focus {
+ background-color: #707070;
+ background-color: rgba(51, 51, 51, 0.7);
+ color: #fff;
+}
+
+/* Footer */
+#infinite-footer {
+ display: none;
+ z-index: 999;
+}
+
+#infinite-footer .container {
+ background-color: #fff;
+ background-color: rgba(255, 255, 255, 0.5);
+ border-color: #eaeaea;
+ border-color: rgba(51, 51, 51, 0.1);
+ padding: 0 0.8em;
+ width: 100% !important;
+}
+
+#infinite-footer .blog-info {
+ font-family: "Noto Sans", sans-serif;
+}
+
+#infinite-footer .blog-info,
+#infinite-footer .blog-credits {
+ height: 24px;
+ line-height: 24px;
+}
+
+#infinite-footer .blog-info a,
+#infinite-footer .blog-credits {
+ font-size: 12px;
+ font-size: 1.2rem;
+}
+
+#infinite-footer .blog-info a,
+#infinite-footer .blog-credits a:hover,
+#infinite-footer .blog-credits a:focus {
+ color: #333;
+}
+
+#infinite-footer .blog-info a:hover,
+#infinite-footer .blog-info a:focus,
+#infinite-footer .blog-credits,
+#infinite-footer .blog-credits a {
+ color: #707070;
+ color: rgba(51, 51, 51, 0.7);
+}
+
+#infinite-footer .blog-info a:hover,
+#infinite-footer .blog-info a:focus,
+#infinite-footer .blog-credits a:hover,
+#infinite-footer .blog-credits a:focus {
+ text-decoration: none;
+}
+
+@media screen and (min-width: 38.75em) {
+ .infinite-loader {
+ margin: 7.6923% 0;
+ }
+
+ .infinite-wrap {
+ margin-top: 7.6923%;
+ }
+
+ #infinite-handle {
+ margin-bottom: 0;
+ }
+}
+
+@media screen and (min-width: 46.25em) {
+ #infinite-handle span {
+ display: block;
+ font-size: 14px;
+ font-size: 1.4rem;
+ padding: 0.8214em
+ }
+}
+
+@media screen and (min-width: 55em) {
+ #infinite-handle span {
+ font-size: 16px;
+ font-size: 1.6rem;
+ padding: 0.8125em;
+ }
+}
+
+@media screen and (min-width: 59.6875em) {
+ .infinite-loader {
+ margin: 8.3333% 0;
+ }
+
+ .infinite-wrap {
+ margin-top: 8.3333%;
+ }
+
+ #infinite-handle {
+ margin: 8.3333% 8.3333% 0;
+ }
+
+ #infinite-handle span {
+ display: inline-block;
+ font-size: 12px;
+ font-size: 1.2rem;
+ padding: 0.7917em 1.5833em;
+ }
+
+ #infinite-footer {
+ display: block;
+ position: fixed;
+ }
+}
+
+@media screen and (min-width: 68.75em) {
+ #infinite-handle span {
+ display: inline-block;
+ font-size: 14px;
+ font-size: 1.4rem;
+ padding: 0.8214em 1.5714em;
+ }
+
+ #infinite-footer .container {
+ padding: 0 0.8235em;
+ }
+
+ #infinite-footer .blog-info,
+ #infinite-footer .blog-credits {
+ height: 27px;
+ line-height: 27px;
+ }
+
+ #infinite-footer .blog-info a {
+ font-size: 14px;
+ font-size: 1.4rem;
+ }
+
+ #infinite-footer .blog-credits {
+ font-size: 12px;
+ font-size: 1.2rem;
+ }
+}
+
+@media screen and (min-width: 77.5em) {
+ #infinite-handle span {
+ font-size: 16px;
+ font-size: 1.6rem;
+ padding: 0.8125em 1.625em;
+ }
+
+ #infinite-footer .container {
+ padding: 0 0.8421em;
+ }
+
+ #infinite-footer .blog-info,
+ #infinite-footer .blog-credits {
+ height: 32px;
+ line-height: 32px;
+ }
+
+ #infinite-footer .blog-info a {
+ font-size: 16px;
+ font-size: 1.6rem;
+ }
+
+ #infinite-footer .blog-credits {
+ font-size: 13px;
+ font-size: 1.3rem;
+ }
+}
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen.php b/plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen.php
new file mode 100644
index 00000000..d917dd56
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyfifteen.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Infinite Scroll Theme Assets
+ *
+ * Register support for Twenty Fifteen.
+ */
+
+/**
+ * Add theme support for infinite scroll
+ */
+function jetpack_twentyfifteen_infinite_scroll_init() {
+ add_theme_support( 'infinite-scroll', array(
+ 'container' => 'main',
+ 'footer' => 'page',
+ ) );
+}
+add_action( 'after_setup_theme', 'jetpack_twentyfifteen_infinite_scroll_init' );
+
+/**
+ * Enqueue CSS stylesheet with theme styles for Infinite Scroll.
+ */
+function jetpack_twentyfifteen_infinite_scroll_enqueue_styles() {
+ if ( wp_script_is( 'the-neverending-homepage' ) ) {
+ wp_enqueue_style( 'infinity-twentyfifteen', plugins_url( 'twentyfifteen.css', __FILE__ ), array( 'the-neverending-homepage' ), '20141022' );
+ wp_style_add_data( 'infinity-twentyfifteen', 'rtl', 'replace' );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'jetpack_twentyfifteen_infinite_scroll_enqueue_styles', 25 );
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.css b/plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.css
new file mode 100644
index 00000000..8fb5647e
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.css
@@ -0,0 +1,111 @@
+/* Spinner */
+
+.infinite-loader {
+ height: 36px;
+ padding: 24px 0;
+}
+
+.infinite-loader .spinner {
+ margin: 0 auto;
+ top: 18px !important;
+ left: 0 !important;
+}
+
+.rtl .infinite-loader .spinner {
+ right: 0 !important;
+ left: auto !important;
+}
+
+/* Click-to-load */
+
+#infinite-handle {
+ padding: 24px 0;
+ text-align: center;
+}
+
+#infinite-handle span {
+ background: #24890d;
+ border-radius: 2px;
+ display: inline-block;
+ color: #fff;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 1;
+ padding: 12px 30px;
+ text-transform: uppercase;
+}
+
+#infinite-handle span:hover {
+ background-color: #41a62a;
+}
+
+#infinite-handle span:active {
+ background-color: #55d737;
+}
+
+/* Footer */
+
+#infinite-footer {
+ z-index: 2;
+}
+
+#infinite-footer .container {
+ margin: 0;
+ padding: 4px 20px;
+}
+
+#infinite-footer .blog-info a {
+ color: #2b2b2b;
+}
+
+#infinite-footer .blog-credits,
+#infinite-footer .blog-credits a {
+ color: #767676;
+}
+
+#infinite-footer .blog-info a:hover,
+#infinite-footer .blog-credits a:hover {
+ color: #41a62a;
+ text-decoration: none;
+}
+
+/* Elements to hide: post navigation, normal footer. */
+
+.infinite-scroll .paging-navigation,
+.infinite-scroll.neverending #colophon {
+ display: none;
+}
+
+@media (max-width: 640px) {
+ #infinite-footer {
+ display: none;
+ }
+}
+
+/* Hooks to infinity-end body class to restore footer. */
+
+.infinity-end.neverending #colophon {
+ display: block;
+}
+
+/* Reset top margin adjustment for subsequent posts added by Infinite Scroll */
+.full-width .site-content .infinite-wrap .hentry.has-post-thumbnail:first-child {
+ margin-top: 0;
+}
+
+@media screen and (min-width: 401px) {
+ .infinite-loader,
+ #infinite-handle {
+ padding: 0 0 48px;
+ }
+
+ .list-view .site-content .infinite-wrap .hentry:first-of-type {
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ padding-top: 48px;
+ }
+
+ .list-view .site-content .infinite-wrap .hentry.has-post-thumbnail {
+ border-top: 0;
+ padding-top: 0;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.php b/plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.php
new file mode 100644
index 00000000..54a1fbc8
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Infinite Scroll Theme Assets
+ *
+ * Register support for Twenty Fourteen.
+ */
+
+/**
+ * Add theme support for infinite scroll
+ */
+function jetpack_twentyfourteen_infinite_scroll_init() {
+ add_theme_support( 'infinite-scroll', array(
+ 'container' => 'content',
+ 'footer' => 'page',
+ 'footer_widgets' => jetpack_twentyfourteen_has_footer_widgets(),
+ ) );
+}
+add_action( 'after_setup_theme', 'jetpack_twentyfourteen_infinite_scroll_init' );
+
+/**
+ * Switch to the "click to load" type IS with the following cases
+ * 1. Viewed from iPad and the primary sidebar is active.
+ * 2. Viewed from mobile and either the primary or the content sidebar is active.
+ * 3. The footer widget is active.
+ *
+ * @return bool
+ */
+function jetpack_twentyfourteen_has_footer_widgets() {
+ if ( function_exists( 'jetpack_is_mobile' ) ) {
+ if ( ( Jetpack_User_Agent_Info::is_ipad() && is_active_sidebar( 'sidebar-1' ) )
+ || ( jetpack_is_mobile( '', true ) && ( is_active_sidebar( 'sidebar-1' ) || is_active_sidebar( 'sidebar-2' ) ) )
+ || is_active_sidebar( 'sidebar-3' ) )
+
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Enqueue CSS stylesheet with theme styles for Infinite Scroll.
+ */
+function jetpack_twentyfourteen_infinite_scroll_enqueue_styles() {
+ if ( wp_script_is( 'the-neverending-homepage' ) ) {
+ wp_enqueue_style( 'infinity-twentyfourteen', plugins_url( 'twentyfourteen.css', __FILE__ ), array( 'the-neverending-homepage' ), '20131118' );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'jetpack_twentyfourteen_infinite_scroll_enqueue_styles', 25 );
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen-rtl.css b/plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen-rtl.css
new file mode 100644
index 00000000..396dfc0d
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen-rtl.css
@@ -0,0 +1,168 @@
+.infinite-scroll .pagination {
+ display: none;
+}
+
+.infinite-wrap > article:before,
+.infinite-wrap > article:after {
+ content: "";
+ display: table;
+}
+
+.infinite-wrap > article:after {
+ clear: both;
+}
+
+.infinite-wrap > article {
+ padding-bottom: 2em;
+}
+
+/* Spinner */
+.site-main .infinite-loader {
+ clear: both;
+ color: currentColor;
+ height: 42px;
+ margin-bottom: 3.5em;
+}
+
+.blog:not(.has-sidebar) .infinite-loader {
+ width: 100%;
+}
+
+.site-main .infinite-loader .spinner {
+ right: 50%!important;
+}
+
+/* Click-to-load */
+#infinite-handle {
+ clear: both;
+ margin: 0 7.6923% 2em;
+ text-align: center;
+}
+
+/* Style "Load More" button */
+.site-main #infinite-handle span {
+ background: #1a1a1a;
+ border-radius: 2px;
+ color: #fff;
+ font-family: "Libre Franklin", "Helvetica Neue", helvetica, arial, sans-serif;
+ font-size: inherit;
+ font-weight: 700;
+ letter-spacing: 0.046875em;
+ line-height: 1;
+ padding: 0.84375em 0.875em 0.78125em;
+ text-transform: uppercase;
+}
+
+#infinite-handle span:hover,
+#infinite-handle span:focus {
+ background: #767676;
+}
+
+/* Style "Load More" button when dark color scheme is used */
+.colors-dark .site-main #infinite-handle span {
+ background: #f8f8f8;
+ border-radius: 2px;
+ color: #222;
+ font-family: "Libre Franklin", "Helvetica Neue", helvetica, arial, sans-serif;
+ font-size: inherit;
+ font-weight: 700;
+ letter-spacing: 0.046875em;
+ line-height: 1;
+ padding: 0.84375em 0.875em 0.78125em;
+ text-transform: uppercase;
+}
+
+.colors-dark #infinite-handle span:hover,
+.colors-dark #infinite-handle span:focus {
+ background: #bbb;
+ columns: #222;
+}
+
+/* Style Infinite Footer */
+#infinite-footer {
+ position: fixed !important;
+}
+
+#infinite-footer .container {
+ background-color: #fff;
+ border-color: #d1d1d1;
+ padding: 0 7.6923%;
+}
+
+#infinite-footer .blog-info,
+#infinite-footer .blog-credits {
+ font-family: "Libre Franklin", "Helvetica Neue", helvetica, arial, sans-serif;
+ text-align: center;
+ width: auto;
+}
+
+#infinite-footer .blog-info a,
+#infinite-footer .blog-credits,
+#infinite-footer .blog-credits a {
+ color: #222222;
+}
+
+#infinite-footer .blog-info a:hover,
+#infinite-footer .blog-info a:focus,
+#infinite-footer .blog-credits a:hover,
+#infinite-footer .blog-credits a:focus {
+ color: #767676;
+ text-decoration: none;
+}
+
+.infinite-scroll #navigation,
+.infinite-scroll.neverending .jetpack-mobile-link,
+.infinite-scroll.neverending .site-footer {
+ display: none;
+}
+
+/* Shows the footer & mobile link again in case all posts have been loaded */
+.infinity-end.neverending .jetpack-mobile-link,
+.infinity-end.neverending .site-footer {
+ display: block;
+}
+
+@media screen and (min-width: 44.375em) {
+ #infinite-handle {
+ margin: 0 0 1em 0;
+ text-align: center;
+ }
+
+ .has-sidebar #infinite-handle {
+ text-align: right;
+ }
+
+ .site-main #infinite-handle span {
+ display: inline-block;
+ }
+}
+
+@media screen and (min-width: 48em) {
+ .infinite-wrap > article {
+ padding-bottom: 4em;
+ }
+}
+
+@media screen and (min-width: 48em) {
+ #infinite-footer .blog-info,
+ #infinite-footer .blog-credits {
+ line-height: 35px;
+ }
+
+ #infinite-footer .blog-info {
+ font-size: 1.1rem;
+ }
+
+ #infinite-footer .blog-credits {
+ font-size: 0.9rem;
+ }
+
+ .blog:not(.has-sidebar) .infinite-loader {
+ float: left;
+ width: 58%;
+ }
+
+ .site-main .infinite-loader .spinner {
+ margin-right: -17px;
+ }
+}
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen.css b/plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen.css
new file mode 100644
index 00000000..243cfb70
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen.css
@@ -0,0 +1,168 @@
+.infinite-scroll .pagination {
+ display: none;
+}
+
+.infinite-wrap > article:before,
+.infinite-wrap > article:after {
+ content: "";
+ display: table;
+}
+
+.infinite-wrap > article:after {
+ clear: both;
+}
+
+.infinite-wrap > article {
+ padding-bottom: 2em;
+}
+
+/* Spinner */
+.site-main .infinite-loader {
+ clear: both;
+ color: currentColor;
+ height: 42px;
+ margin-bottom: 3.5em;
+}
+
+.blog:not(.has-sidebar) .infinite-loader {
+ width: 100%;
+}
+
+.site-main .infinite-loader .spinner {
+ left: 50%!important;
+}
+
+/* Click-to-load */
+#infinite-handle {
+ clear: both;
+ margin: 0 7.6923% 2em;
+ text-align: center;
+}
+
+/* Style "Load More" button */
+.site-main #infinite-handle span {
+ background: #1a1a1a;
+ border-radius: 2px;
+ color: #fff;
+ font-family: "Libre Franklin", "Helvetica Neue", helvetica, arial, sans-serif;
+ font-size: inherit;
+ font-weight: 700;
+ letter-spacing: 0.046875em;
+ line-height: 1;
+ padding: 0.84375em 0.875em 0.78125em;
+ text-transform: uppercase;
+}
+
+#infinite-handle span:hover,
+#infinite-handle span:focus {
+ background: #767676;
+}
+
+/* Style "Load More" button when dark color scheme is used */
+.colors-dark .site-main #infinite-handle span {
+ background: #f8f8f8;
+ border-radius: 2px;
+ color: #222;
+ font-family: "Libre Franklin", "Helvetica Neue", helvetica, arial, sans-serif;
+ font-size: inherit;
+ font-weight: 700;
+ letter-spacing: 0.046875em;
+ line-height: 1;
+ padding: 0.84375em 0.875em 0.78125em;
+ text-transform: uppercase;
+}
+
+.colors-dark #infinite-handle span:hover,
+.colors-dark #infinite-handle span:focus {
+ background: #bbb;
+ columns: #222;
+}
+
+/* Style Infinite Footer */
+#infinite-footer {
+ position: fixed !important;
+}
+
+#infinite-footer .container {
+ background-color: #fff;
+ border-color: #d1d1d1;
+ padding: 0 7.6923%;
+}
+
+#infinite-footer .blog-info,
+#infinite-footer .blog-credits {
+ font-family: "Libre Franklin", "Helvetica Neue", helvetica, arial, sans-serif;
+ text-align: center;
+ width: auto;
+}
+
+#infinite-footer .blog-info a,
+#infinite-footer .blog-credits,
+#infinite-footer .blog-credits a {
+ color: #222222;
+}
+
+#infinite-footer .blog-info a:hover,
+#infinite-footer .blog-info a:focus,
+#infinite-footer .blog-credits a:hover,
+#infinite-footer .blog-credits a:focus {
+ color: #767676;
+ text-decoration: none;
+}
+
+.infinite-scroll #navigation,
+.infinite-scroll.neverending .jetpack-mobile-link,
+.infinite-scroll.neverending .site-footer {
+ display: none;
+}
+
+/* Shows the footer & mobile link again in case all posts have been loaded */
+.infinity-end.neverending .jetpack-mobile-link,
+.infinity-end.neverending .site-footer {
+ display: block;
+}
+
+@media screen and (min-width: 44.375em) {
+ #infinite-handle {
+ margin: 0 0 1em 0;
+ text-align: center;
+ }
+
+ .has-sidebar #infinite-handle {
+ text-align: left;
+ }
+
+ .site-main #infinite-handle span {
+ display: inline-block;
+ }
+}
+
+@media screen and (min-width: 48em) {
+ .infinite-wrap > article {
+ padding-bottom: 4em;
+ }
+}
+
+@media screen and (min-width: 48em) {
+ #infinite-footer .blog-info,
+ #infinite-footer .blog-credits {
+ line-height: 35px;
+ }
+
+ #infinite-footer .blog-info {
+ font-size: 1.1rem;
+ }
+
+ #infinite-footer .blog-credits {
+ font-size: 0.9rem;
+ }
+
+ .blog:not(.has-sidebar) .infinite-loader {
+ float: right;
+ width: 58%;
+ }
+
+ .site-main .infinite-loader .spinner {
+ margin-left: -17px;
+ }
+}
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen.php b/plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen.php
new file mode 100644
index 00000000..ca4c64c7
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyseventeen.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Infinite Scroll Theme Assets
+ *
+ * Register support for Twenty Seventeen.
+ */
+
+/**
+ * Add theme support for infinite scroll
+ */
+function jetpack_twentyseventeen_infinite_scroll_init() {
+ add_theme_support( 'infinite-scroll', array(
+ 'container' => 'main',
+ 'render' => 'jetpack_twentyseventeen_infinite_scroll_render',
+ 'footer' => 'content',
+ 'footer_widgets' => jetpack_twentyseventeen_has_footer_widgets(),
+ ) );
+}
+add_action( 'init', 'jetpack_twentyseventeen_infinite_scroll_init' );
+
+/**
+ * Custom render function for Infinite Scroll.
+ */
+function jetpack_twentyseventeen_infinite_scroll_render() {
+ while ( have_posts() ) {
+ the_post();
+ if ( is_search() ) {
+ get_template_part( 'template-parts/post/content', 'search' );
+ } else {
+ get_template_part( 'template-parts/post/content', get_post_format() );
+ }
+ }
+}
+
+/**
+ * Custom function to check for the presence of footer widgets or the social links menu
+ */
+function jetpack_twentyseventeen_has_footer_widgets() {
+ if ( is_active_sidebar( 'sidebar-2' ) ||
+ is_active_sidebar( 'sidebar-3' ) ||
+ has_nav_menu( 'social' ) ) {
+
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Enqueue CSS stylesheet with theme styles for Infinite Scroll.
+ */
+function jetpack_twentyseventeen_infinite_scroll_enqueue_styles() {
+ if ( wp_script_is( 'the-neverending-homepage' ) ) {
+ wp_enqueue_style( 'infinity-twentyseventeen', plugins_url( 'twentyseventeen.css', __FILE__ ), array( 'the-neverending-homepage' ), '20161219' );
+ wp_style_add_data( 'infinity-twentyseventeen', 'rtl', 'replace' );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'jetpack_twentyseventeen_infinite_scroll_enqueue_styles', 25 );
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentysixteen-rtl.css b/plugins/jetpack/modules/infinite-scroll/themes/twentysixteen-rtl.css
new file mode 100644
index 00000000..0b650cee
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentysixteen-rtl.css
@@ -0,0 +1,161 @@
+.infinite-scroll .pagination {
+ display: none;
+}
+
+.infinite-wrap > article:before,
+.infinite-wrap > article:after {
+ content: "";
+ display: table;
+}
+
+.infinite-wrap > article:after {
+ clear: both;
+}
+
+.infinite-wrap > article {
+ margin-bottom: 3.5em;
+}
+
+/* Spinner */
+.site-main .infinite-loader {
+ clear: both;
+ color: currentColor;
+ height: 42px;
+ margin-bottom: 3.5em;
+}
+
+.infinite-loader .spinner {
+ right: 50% !important;
+ top: 50% !important;
+}
+
+/* Click-to-load */
+#infinite-handle {
+ clear: both;
+ margin-right: 7.6923%;
+ margin-left: 7.6923%;
+ text-align: center;
+}
+
+.site-main #infinite-handle span {
+ background: #1a1a1a;
+ border-radius: 2px;
+ color: #fff;
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+ font-size: inherit;
+ font-weight: 700;
+ letter-spacing: 0.046875em;
+ line-height: 1;
+ padding: 0.84375em 0.875em 0.78125em;
+ text-transform: uppercase;
+}
+
+#infinite-handle span:hover,
+#infinite-handle span:focus {
+ background: #007acc;
+}
+
+#infinite-handle button:focus {
+ outline-offset: 0.375em;
+}
+
+/* Footer */
+body #infinite-footer {
+ display: none;
+ z-index: 999;
+}
+
+body #infinite-footer .container {
+ background-color: #fff;
+ background-color: rgba(255, 255, 255, 0.8);
+ border-color: #d1d1d1;
+ padding: 0 7.6923%;
+ width: 100% !important;
+}
+
+body #infinite-footer .blog-info {
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+ height: 2.1875em;
+ line-height: 2.1875em;
+}
+
+body #infinite-footer .blog-info a {
+ color: #1a1a1a;
+ font-size: inherit
+}
+
+body #infinite-footer .blog-credits {
+ font-size: 13px;
+ font-size: 0.8125rem;
+ height: 2.692307692em;
+ line-height: 2.692307692em;
+}
+
+body #infinite-footer .blog-credits,
+body #infinite-footer .blog-credits a {
+ color: #686868;
+}
+
+body #infinite-footer .blog-info a:hover,
+body #infinite-footer .blog-info a:focus,
+body #infinite-footer .blog-credits a:hover,
+body #infinite-footer .blog-credits a:focus {
+ color: #007acc;
+ text-decoration: none;
+}
+
+@media screen and (min-width: 44.375em) {
+ .infinite-wrap > article,
+ .site-main .infinite-loader {
+ margin-bottom: 5.25em;
+ }
+
+ .infinite-loader .spinner {
+ right: 7.6923% !important;
+ margin-right: 12px;
+ }
+
+ #infinite-handle {
+ text-align: right;
+ }
+
+ .site-main #infinite-handle span {
+ display: inline-block;
+ }
+
+ body #infinite-footer .container {
+ padding: 0 0.761904762em;
+ width: -webkit-calc(100% - 42px) !important;
+ width: calc(100% - 42px) !important;
+ }
+
+ body:not(.custom-background-image) #infinite-footer {
+ bottom: 21px !important;
+ }
+}
+
+@media screen and (min-width: 56.875em) {
+ .infinite-loader .spinner {
+ right: 0 !important;
+ }
+
+ #infinite-handle {
+ margin: 0;
+ }
+
+ .no-sidebar .infinite-loader .spinner {
+ right: 50% !important;
+ margin: 0;
+ }
+
+ .no-sidebar #infinite-handle {
+ text-align: center;
+ }
+}
+
+@media screen and (min-width: 61.5625em) {
+ .infinite-wrap > article,
+ .site-main .infinite-loader {
+ margin-bottom: 7.0em;
+ }
+}
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentysixteen.css b/plugins/jetpack/modules/infinite-scroll/themes/twentysixteen.css
new file mode 100644
index 00000000..3d2cd8f2
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentysixteen.css
@@ -0,0 +1,161 @@
+.infinite-scroll .pagination {
+ display: none;
+}
+
+.infinite-wrap > article:before,
+.infinite-wrap > article:after {
+ content: "";
+ display: table;
+}
+
+.infinite-wrap > article:after {
+ clear: both;
+}
+
+.infinite-wrap > article {
+ margin-bottom: 3.5em;
+}
+
+/* Spinner */
+.site-main .infinite-loader {
+ clear: both;
+ color: currentColor;
+ height: 42px;
+ margin-bottom: 3.5em;
+}
+
+.infinite-loader .spinner {
+ left: 50% !important;
+ top: 50% !important;
+}
+
+/* Click-to-load */
+#infinite-handle {
+ clear: both;
+ margin-right: 7.6923%;
+ margin-left: 7.6923%;
+ text-align: center;
+}
+
+.site-main #infinite-handle span {
+ background: #1a1a1a;
+ border-radius: 2px;
+ color: #fff;
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+ font-size: inherit;
+ font-weight: 700;
+ letter-spacing: 0.046875em;
+ line-height: 1;
+ padding: 0.84375em 0.875em 0.78125em;
+ text-transform: uppercase;
+}
+
+#infinite-handle span:hover,
+#infinite-handle span:focus {
+ background: #007acc;
+}
+
+#infinite-handle button:focus {
+ outline-offset: 0.375em;
+}
+
+/* Footer */
+body #infinite-footer {
+ display: none;
+ z-index: 999;
+}
+
+body #infinite-footer .container {
+ background-color: #fff;
+ background-color: rgba(255, 255, 255, 0.8);
+ border-color: #d1d1d1;
+ padding: 0 7.6923%;
+ width: 100% !important;
+}
+
+body #infinite-footer .blog-info {
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+ height: 2.1875em;
+ line-height: 2.1875em;
+}
+
+body #infinite-footer .blog-info a {
+ color: #1a1a1a;
+ font-size: inherit
+}
+
+body #infinite-footer .blog-credits {
+ font-size: 13px;
+ font-size: 0.8125rem;
+ height: 2.692307692em;
+ line-height: 2.692307692em;
+}
+
+body #infinite-footer .blog-credits,
+body #infinite-footer .blog-credits a {
+ color: #686868;
+}
+
+body #infinite-footer .blog-info a:hover,
+body #infinite-footer .blog-info a:focus,
+body #infinite-footer .blog-credits a:hover,
+body #infinite-footer .blog-credits a:focus {
+ color: #007acc;
+ text-decoration: none;
+}
+
+@media screen and (min-width: 44.375em) {
+ .infinite-wrap > article,
+ .site-main .infinite-loader {
+ margin-bottom: 5.25em;
+ }
+
+ .infinite-loader .spinner {
+ left: 7.6923% !important;
+ margin-left: 12px;
+ }
+
+ #infinite-handle {
+ text-align: left;
+ }
+
+ .site-main #infinite-handle span {
+ display: inline-block;
+ }
+
+ body #infinite-footer .container {
+ padding: 0 0.761904762em;
+ width: -webkit-calc(100% - 42px) !important;
+ width: calc(100% - 42px) !important;
+ }
+
+ body:not(.custom-background-image) #infinite-footer {
+ bottom: 21px !important;
+ }
+}
+
+@media screen and (min-width: 56.875em) {
+ .infinite-loader .spinner {
+ left: 0 !important;
+ }
+
+ #infinite-handle {
+ margin: 0;
+ }
+
+ .no-sidebar .infinite-loader .spinner {
+ left: 50% !important;
+ margin: 0;
+ }
+
+ .no-sidebar #infinite-handle {
+ text-align: center;
+ }
+}
+
+@media screen and (min-width: 61.5625em) {
+ .infinite-wrap > article,
+ .site-main .infinite-loader {
+ margin-bottom: 7.0em;
+ }
+}
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentysixteen.php b/plugins/jetpack/modules/infinite-scroll/themes/twentysixteen.php
new file mode 100644
index 00000000..df0c14e9
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentysixteen.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Infinite Scroll Theme Assets
+ *
+ * Register support for Twenty Sixteen.
+ */
+
+/**
+ * Add theme support for infinite scroll
+ */
+function jetpack_twentysixteen_infinite_scroll_init() {
+ add_theme_support( 'infinite-scroll', array(
+ 'container' => 'main',
+ 'render' => 'jetpack_twentysixteen_infinite_scroll_render',
+ 'footer' => 'content',
+ ) );
+}
+add_action( 'after_setup_theme', 'jetpack_twentysixteen_infinite_scroll_init' );
+
+/**
+ * Custom render function for Infinite Scroll.
+ */
+function jetpack_twentysixteen_infinite_scroll_render() {
+ while ( have_posts() ) {
+ the_post();
+ if ( is_search() ) {
+ get_template_part( 'template-parts/content', 'search' );
+ } else {
+ get_template_part( 'template-parts/content', get_post_format() );
+ }
+ }
+}
+
+/**
+ * Enqueue CSS stylesheet with theme styles for Infinite Scroll.
+ */
+function jetpack_twentysixteen_infinite_scroll_enqueue_styles() {
+ if ( wp_script_is( 'the-neverending-homepage' ) ) {
+ wp_enqueue_style( 'infinity-twentysixteen', plugins_url( 'twentysixteen.css', __FILE__ ), array( 'the-neverending-homepage' ), '20151102' );
+ wp_style_add_data( 'infinity-twentysixteen', 'rtl', 'replace' );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'jetpack_twentysixteen_infinite_scroll_enqueue_styles', 25 );
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyten.css b/plugins/jetpack/modules/infinite-scroll/themes/twentyten.css
new file mode 100644
index 00000000..889abb3f
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyten.css
@@ -0,0 +1,25 @@
+/* =Infinity Styles
+-------------------------------------------------------------- */
+.infinite-scroll #wrapper {
+ margin-bottom: 40px;
+}
+.infinite-scroll #content {
+ margin-bottom: 50px;
+}
+.infinite-scroll #content .infinite-wrap {
+ padding-top: 0;
+ border-top: 0;
+}
+/* Elements to hide */
+.infinite-scroll #nav-above,
+.infinite-scroll #nav-below,
+.infinite-scroll.neverending #footer {
+ display: none;
+}
+/* Restore the footer when IS is finished */
+.infinity-end.neverending #footer {
+ display: block;
+}
+#infinite-footer .blog-info a {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyten.php b/plugins/jetpack/modules/infinite-scroll/themes/twentyten.php
new file mode 100644
index 00000000..b6128707
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyten.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Infinite Scroll Theme Assets
+ *
+ * Register support for @Twenty Ten and enqueue relevant styles.
+ */
+
+/**
+ * Add theme support for infinity scroll
+ */
+function jetpack_twentyten_infinite_scroll_init() {
+ add_theme_support( 'infinite-scroll', array(
+ 'container' => 'content',
+ 'render' => 'jetpack_twentyten_infinite_scroll_render',
+ 'footer' => 'wrapper',
+ 'footer_widgets' => jetpack_twentyten_has_footer_widgets(),
+ ) );
+}
+add_action( 'init', 'jetpack_twentyten_infinite_scroll_init' );
+
+/**
+ * Set the code to be rendered on for calling posts,
+ * hooked to template parts when possible.
+ *
+ * Note: must define a loop.
+ */
+function jetpack_twentyten_infinite_scroll_render() {
+ get_template_part( 'loop' );
+}
+
+/**
+ * Enqueue CSS stylesheet with theme styles for infinity.
+ */
+function jetpack_twentyten_infinite_scroll_enqueue_styles() {
+ if ( wp_script_is( 'the-neverending-homepage' ) ) {
+ // Add theme specific styles.
+ wp_enqueue_style( 'infinity-twentyten', plugins_url( 'twentyten.css', __FILE__ ), array( 'the-neverending-homepage' ), '20121002' );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'jetpack_twentyten_infinite_scroll_enqueue_styles', 25 );
+
+/**
+ * Do we have footer widgets?
+ */
+function jetpack_twentyten_has_footer_widgets() {
+ if ( is_active_sidebar( 'first-footer-widget-area' ) ||
+ is_active_sidebar( 'second-footer-widget-area' ) ||
+ is_active_sidebar( 'third-footer-widget-area' ) ||
+ is_active_sidebar( 'fourth-footer-widget-area' ) ) {
+
+ return true;
+ }
+
+ return false;
+}
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentythirteen.css b/plugins/jetpack/modules/infinite-scroll/themes/twentythirteen.css
new file mode 100644
index 00000000..ba169b35
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentythirteen.css
@@ -0,0 +1,90 @@
+/* =Infinite Scroll
+-------------------------------------------------------------- */
+
+.infinite-wrap {
+ border-top: 0;
+}
+
+/* Spinner */
+.infinite-loader {
+ background-color: #e8e5ce;
+ padding: 40px 0;
+}
+.infinite-loader .spinner {
+ margin: 0 auto;
+ width: 34px;
+ height: 34px;
+}
+.sidebar .infinite-loader .spinner {
+ padding-right: 376px;
+}
+.rtl.sidebar .infinite-loader .spinner {
+ padding-left: 376px;
+ padding-right: 0;
+}
+
+/* Click-to-load */
+#infinite-handle {
+ background-color: #e8e5ce;
+ padding: 40px 0;
+ text-align: center;
+}
+.sidebar #infinite-handle {
+ padding-right: 376px;
+}
+.rtl.sidebar #infinite-handle {
+ padding-left: 376px;
+ padding-right: 0;
+}
+#infinite-handle span {
+ background: #e05d22; /* Old browsers */
+ background: -webkit-linear-gradient(top, #e05d22 0%, #d94412 100%); /* Chrome 10+, Safari 5.1+ */
+ background: linear-gradient(to bottom, #e05d22 0%, #d94412 100%); /* W3C */
+ border: none;
+ border-bottom: 3px solid #b93207;
+ border-radius: 2px;
+ display: inline-block;
+ color: #fff;
+ font-size: 100%;
+ padding: 11px 24px 10px;
+ text-decoration: none;
+}
+#infinite-handle span:hover {
+ background: #ed6a31; /* Old browsers */
+ background: -webkit-linear-gradient(top, #ed6a31 0%, #e55627 100%); /* Chrome 10+, Safari 5.1+ */
+ background: linear-gradient(to bottom, #ed6a31 0%, #e55627 100%); /* W3C */
+ outline: none;
+}
+#infinite-handle span:active {
+ background: #d94412; /* Old browsers */
+ background: -webkit-linear-gradient(top, #d94412 0%, #e05d22 100%); /* Chrome 10+, Safari 5.1+ */
+ background: linear-gradient(to bottom, #d94412 0%, #e05d22 100%); /* W3C */
+ border: none;
+ border-top: 3px solid #b93207;
+ padding: 10px 24px 11px;
+}
+
+/* Elements to hide: post navigation, normal footer. */
+.infinite-scroll .paging-navigation,
+.infinite-scroll.neverending #colophon {
+ display: none;
+}
+
+/* Hooks to infinity-end body class to restore footer. */
+.infinity-end.neverending #colophon {
+ display: block;
+}
+
+/* For small viewports. */
+@media (max-width: 999px) {
+ .sidebar .infinite-loader .spinner,
+ .rtl.sidebar .infinite-loader .spinner {
+ padding-right: 0;
+ padding-left: 0;
+ }
+ .infinite-scroll #infinite-handle,
+ .rtl.sidebar #infinite-handle {
+ padding-right: 0;
+ padding-left: 0;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentythirteen.php b/plugins/jetpack/modules/infinite-scroll/themes/twentythirteen.php
new file mode 100644
index 00000000..803b31bb
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentythirteen.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Infinite Scroll Theme Assets
+ *
+ * Register support for Twenty Thirteen.
+ */
+
+/**
+ * Add theme support for infinite scroll
+ */
+function jetpack_twentythirteen_infinite_scroll_init() {
+ add_theme_support( 'infinite-scroll', array(
+ 'container' => 'content',
+ 'footer' => 'page',
+ 'footer_widgets' => array( 'sidebar-1' ),
+ ) );
+}
+add_action( 'after_setup_theme', 'jetpack_twentythirteen_infinite_scroll_init' );
+
+/**
+ * Enqueue CSS stylesheet with theme styles for Infinite Scroll.
+ */
+function jetpack_twentythirteen_infinite_scroll_enqueue_styles() {
+ if ( wp_script_is( 'the-neverending-homepage' ) ) {
+ wp_enqueue_style( 'infinity-twentythirteen', plugins_url( 'twentythirteen.css', __FILE__ ), array( 'the-neverending-homepage' ), '20130409' );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'jetpack_twentythirteen_infinite_scroll_enqueue_styles', 25 );
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentytwelve.css b/plugins/jetpack/modules/infinite-scroll/themes/twentytwelve.css
new file mode 100644
index 00000000..032c2c9a
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentytwelve.css
@@ -0,0 +1,33 @@
+/* =Infinity Styles
+-------------------------------------------------------------- */
+.infinite-scroll .site-content:after {
+ clear: both;
+ content: '';
+ display: block;
+}
+.infinite-wrap {
+ border-top: 0;
+}
+.infinite-scroll.neverending .site-content {
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+}
+
+/* Elements to hide: post navigation, regular footer */
+.infinite-scroll #nav-below,
+.infinite-scroll.neverending #colophon {
+ display: none;
+}
+
+/* Hooks to infinity-end body class to restore footer */
+.infinity-end.neverending #colophon {
+ display: block;
+}
+
+/* For responsive CSS */
+@media (max-width: 599px) {
+ .infinite-scroll #infinite-handle {
+ padding-bottom: 48px;
+ padding-bottom: 3.428571429rem;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentytwelve.php b/plugins/jetpack/modules/infinite-scroll/themes/twentytwelve.php
new file mode 100644
index 00000000..b8b17b3f
--- /dev/null
+++ b/plugins/jetpack/modules/infinite-scroll/themes/twentytwelve.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Infinite Scroll Theme Assets
+ *
+ * Register support for Twenty Twelve and enqueue relevant styles.
+ */
+
+/**
+ * Add theme support for infinite scroll
+ */
+function jetpack_twentytwelve_infinite_scroll_init() {
+ add_theme_support( 'infinite-scroll', array(
+ 'container' => 'content',
+ 'footer' => 'page',
+ 'footer_widgets' => jetpack_twentytwelve_has_footer_widgets(),
+ ) );
+}
+add_action( 'after_setup_theme', 'jetpack_twentytwelve_infinite_scroll_init' );
+
+/**
+ * Enqueue CSS stylesheet with theme styles for infinity.
+ */
+function jetpack_twentytwelve_infinite_scroll_enqueue_styles() {
+ if ( wp_script_is( 'the-neverending-homepage' ) ) {
+ // Add theme specific styles.
+ wp_enqueue_style( 'infinity-twentytwelve', plugins_url( 'twentytwelve.css', __FILE__ ), array( 'the-neverending-homepage' ), '20120817' );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'jetpack_twentytwelve_infinite_scroll_enqueue_styles', 25 );
+
+/**
+ * Do we have footer widgets?
+ */
+function jetpack_twentytwelve_has_footer_widgets() {
+ if ( function_exists( 'jetpack_is_mobile' ) && jetpack_is_mobile() ) {
+ if ( is_front_page() && ( is_active_sidebar( 'sidebar-2' ) || is_active_sidebar( 'sidebar-3' ) ) )
+ return true;
+ elseif ( is_active_sidebar( 'sidebar-1' ) )
+ return true;
+ }
+
+ return false;
+}
diff --git a/plugins/jetpack/modules/json-api.php b/plugins/jetpack/modules/json-api.php
new file mode 100644
index 00000000..b30f38a9
--- /dev/null
+++ b/plugins/jetpack/modules/json-api.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Module Name: JSON API
+ * Module Description: Allow applications to securely access your content.
+ * Sort Order: 19
+ * First Introduced: 1.9
+ * Requires Connection: Yes
+ * Auto Activate: Public
+ * Module Tags: Writing, Developers
+ * Feature: General
+ * Additional Search Queries: api, rest, develop, developers, json, klout, oauth
+ */
diff --git a/plugins/jetpack/modules/latex.php b/plugins/jetpack/modules/latex.php
new file mode 100644
index 00000000..340dfa9f
--- /dev/null
+++ b/plugins/jetpack/modules/latex.php
@@ -0,0 +1,122 @@
+<?php
+/**
+ * Module Name: Beautiful Math
+ * Module Description: Use the LaTeX markup language to write mathematical equations and formulas.
+ * Sort Order: 12
+ * First Introduced: 1.1
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Writing
+ * Feature: Writing
+ * Additional Search Queries: latex, math, equation, equations, formula, code
+ */
+
+/**
+ * LaTeX support.
+ *
+ * Backward compatibility requires support for both "[latex][/latex]", and
+ * "$latex $" shortcodes.
+ *
+ * $latex e^{\i \pi} + 1 = 0$ -> [latex]e^{\i \pi} + 1 = 0[/latex]
+ * $latex [a, b]$ -> [latex][a, b][/latex]
+ */
+
+function latex_markup( $content ) {
+ $textarr = wp_html_split( $content );
+
+ $regex = '%
+ \$latex(?:=\s*|\s+)
+ ((?:
+ [^$]+ # Not a dollar
+ |
+ (?<=(?<!\\\\)\\\\)\$ # Dollar preceded by exactly one slash
+ )+)
+ (?<!\\\\)\$ # Dollar preceded by zero slashes
+ %ix';
+
+ foreach ( $textarr as &$element ) {
+ if ( '' == $element || '<' === $element[0] ) {
+ continue;
+ }
+
+ if ( false === stripos( $element, '$latex' ) ) {
+ continue;
+ }
+
+ $element = preg_replace_callback( $regex, 'latex_src', $element );
+ }
+
+ return implode( '', $textarr );
+}
+
+function latex_src( $matches ) {
+ $latex = $matches[1];
+
+ $bg = latex_get_default_color( 'bg' );
+ $fg = latex_get_default_color( 'text', '000' );
+ $s = 0;
+
+
+ $latex = latex_entity_decode( $latex );
+ if ( preg_match( '/.+(&fg=[0-9a-f]{6}).*/i', $latex, $fg_matches ) ) {
+ $fg = substr( $fg_matches[1], 4 );
+ $latex = str_replace( $fg_matches[1], '', $latex );
+ }
+ if ( preg_match( '/.+(&bg=[0-9a-f]{6}).*/i', $latex, $bg_matches ) ) {
+ $bg = substr( $bg_matches[1], 4 );
+ $latex = str_replace( $bg_matches[1], '', $latex );
+ }
+ if ( preg_match( '/.+(&s=[0-9-]{1,2}).*/i', $latex, $s_matches ) ) {
+ $s = (int) substr( $s_matches[1], 3 );
+ $latex = str_replace( $s_matches[1], '', $latex );
+ }
+
+ return latex_render( $latex, $fg, $bg, $s );
+}
+
+function latex_get_default_color( $color, $default_color = 'ffffff' ) {
+ global $themecolors;
+ return isset($themecolors[$color]) ? $themecolors[$color] : $default_color;
+}
+
+function latex_entity_decode( $latex ) {
+ return str_replace( array( '&lt;', '&gt;', '&quot;', '&#039;', '&#038;', '&amp;', "\n", "\r" ), array( '<', '>', '"', "'", '&', '&', ' ', ' ' ), $latex );
+}
+
+function latex_render( $latex, $fg, $bg, $s = 0 ) {
+ $url = "//s0.wp.com/latex.php?latex=" . urlencode( $latex ) . "&bg=" . $bg . "&fg=" . $fg . "&s=" . $s;
+ $url = esc_url( $url );
+ $alt = str_replace( '\\', '&#92;', esc_attr( $latex ) );
+
+ return '<img src="' . $url . '" alt="' . $alt . '" title="' . $alt . '" class="latex" />';
+}
+
+/**
+ * The shortcode way. The attributes are the same as the old ones - 'fg' and 'bg', instead of foreground
+ * and background, and 's' is for the font size.
+ *
+ * Example: [latex s=4 bg=00f fg=ff0]\LaTeX[/latex]
+ */
+function latex_shortcode( $atts, $content = '' ) {
+ extract( shortcode_atts( array(
+ 's' => 0,
+ 'bg' => latex_get_default_color( 'bg' ),
+ 'fg' => latex_get_default_color( 'text', '000' )
+ ), $atts, 'latex' ) );
+
+ return latex_render( latex_entity_decode( $content ), $fg, $bg, $s );
+}
+
+/**
+ * LaTeX needs to be untexturized
+ */
+function latex_no_texturize( $shortcodes ) {
+ $shortcodes[] = 'latex';
+ return $shortcodes;
+}
+
+add_filter( 'no_texturize_shortcodes', 'latex_no_texturize' );
+
+add_filter( 'the_content', 'latex_markup', 9 ); // before wptexturize
+add_filter( 'comment_text', 'latex_markup', 9 ); // before wptexturize
+add_shortcode( 'latex', 'latex_shortcode' );
diff --git a/plugins/jetpack/modules/lazy-images.php b/plugins/jetpack/modules/lazy-images.php
new file mode 100644
index 00000000..57df7706
--- /dev/null
+++ b/plugins/jetpack/modules/lazy-images.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * Module Name: Lazy Images
+ * Module Description: Speed up your site and create a smoother viewing experience by loading images as visitors scroll down the screen, instead of all at once.
+ * Jumpstart Description: Lazy-loading images improve your site's speed and create a smoother viewing experience. Images will load as visitors scroll down the screen, instead of all at once.
+ * Sort Order: 24
+ * Recommendation Order: 14
+ * First Introduced: 5.6.0
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Appearance, Recommended
+ * Feature: Appearance, Jumpstart
+ * Additional Search Queries: mobile, theme, fast images, fast image, image, lazy, lazy load, lazyload, images, lazy images, thumbnail, image lazy load, lazy loading, load, loading
+ */
+
+/**
+ * This module relies heavily upon the Lazy Load plugin which was worked on by
+ * Mohammad Jangda (batmoo), the WordPress.com VIP team, the TechCrunch 2011
+ * redesign team, and Jake Goldman of 10up LLC.
+ *
+ * The JavaScript has been updated to rely on InterSection observer instead of
+ * jQuery Sonar. Many thanks to Dean Hume (deanhume) and his example:
+ * https://github.com/deanhume/lazy-observer-load
+ */
+
+require_once( JETPACK__PLUGIN_DIR . 'modules/lazy-images/lazy-images.php' );
+
+/*
+ * Initialize lazy images on the wp action so that conditional
+ * tags are safe to use.
+ *
+ * As an example, this is important if a theme wants to disable lazy images except
+ * on single posts, pages, or attachments by short-circuiting lazy images when
+ * is_singular() returns false.
+ *
+ * See: https://github.com/Automattic/jetpack/issues/8888
+ */
+
+add_action( 'wp', array( 'Jetpack_Lazy_Images', 'instance' ) );
diff --git a/plugins/jetpack/modules/lazy-images/images/1x1.trans.gif b/plugins/jetpack/modules/lazy-images/images/1x1.trans.gif
new file mode 100644
index 00000000..f191b280
--- /dev/null
+++ b/plugins/jetpack/modules/lazy-images/images/1x1.trans.gif
Binary files differ
diff --git a/plugins/jetpack/modules/lazy-images/js/lazy-images.js b/plugins/jetpack/modules/lazy-images/js/lazy-images.js
new file mode 100644
index 00000000..31286bd3
--- /dev/null
+++ b/plugins/jetpack/modules/lazy-images/js/lazy-images.js
@@ -0,0 +1,870 @@
+/* globals IntersectionObserver, jQuery */
+
+var jetpackLazyImagesModule = function( $ ) {
+ var images,
+ config = {
+ // If the image gets within 200px in the Y axis, start the download.
+ rootMargin: '200px 0px',
+ threshold: 0.01
+ },
+ imageCount = 0,
+ observer,
+ image,
+ i;
+
+ $( document ).ready( function() {
+ lazy_load_init();
+
+ // Lazy load images that are brought in from Infinite Scroll
+ $( 'body' ).bind( 'post-load', lazy_load_init );
+
+ // Add event to provide optional compatibility for other code.
+ $( 'body' ).bind( 'jetpack-lazy-images-load', lazy_load_init );
+ } );
+
+ function lazy_load_init() {
+ images = document.querySelectorAll( 'img.jetpack-lazy-image:not(.jetpack-lazy-image--handled)' );
+ imageCount = images.length;
+
+ // If initialized, then disconnect the observer
+ if ( observer ) {
+ observer.disconnect();
+ }
+
+ // If we don't have support for intersection observer, loads the images immediately
+ if ( ! ( 'IntersectionObserver' in window ) ) {
+ loadImagesImmediately( images );
+ } else {
+ // It is supported, load the images
+ observer = new IntersectionObserver( onIntersection, config );
+
+ // foreach() is not supported in IE
+ for ( i = 0; i < images.length; i++ ) {
+ image = images[ i ];
+ if ( image.getAttribute( 'data-lazy-loaded' ) ) {
+ continue;
+ }
+
+ observer.observe( image );
+ }
+ }
+ }
+
+ /**
+ * Load all of the images immediately
+ * @param {NodeListOf<Element>} immediateImages List of lazy-loaded images to load immediately.
+ */
+ function loadImagesImmediately( immediateImages ) {
+ var i;
+
+ // foreach() is not supported in IE
+ for ( i = 0; i < immediateImages.length; i++ ) {
+ var image = immediateImages[ i ];
+ applyImage( image );
+ }
+ }
+
+ /**
+ * On intersection
+ * @param {array} entries List of elements being observed.
+ */
+ function onIntersection( entries ) {
+ var i;
+
+ // Disconnect if we've already loaded all of the images
+ if ( imageCount === 0 ) {
+ observer.disconnect();
+ }
+
+ // Loop through the entries
+ for ( i = 0; i < entries.length; i++ ) {
+ var entry = entries[ i ];
+
+ // Are we in viewport?
+ if ( entry.intersectionRatio > 0 ) {
+ imageCount--;
+
+ // Stop watching and load the image
+ observer.unobserve( entry.target );
+ applyImage( entry.target );
+ }
+ }
+ }
+
+ /**
+ * Apply the image
+ * @param {object} image The image object.
+ */
+ function applyImage( image ) {
+ var theImage = $( image ),
+ srcset,
+ sizes,
+ theClone;
+
+ if ( ! theImage.length ) {
+ return;
+ }
+
+ srcset = theImage.attr( 'data-lazy-srcset' );
+ sizes = theImage.attr( 'data-lazy-sizes' );
+ theClone = theImage.clone();
+
+ // Remove lazy attributes from the clone.
+ theClone.removeAttr( 'data-lazy-srcset' ),
+ theClone.removeAttr( 'data-lazy-sizes' );
+ theClone.removeAttr( 'data-lazy-src' );
+
+ // Add the attributes we want on the finished image.
+ theClone.addClass( 'jetpack-lazy-image--handled' );
+ theClone.attr( 'data-lazy-loaded', 1 );
+ if ( ! srcset ) {
+ theClone.removeAttr( 'srcset' );
+ } else {
+ theClone.attr( 'srcset', srcset );
+ }
+ if ( sizes ) {
+ theClone.attr( 'sizes', sizes );
+ }
+
+ theImage.replaceWith( theClone );
+
+ // Fire an event so that third-party code can perform actions after an image is loaded.
+ theClone.trigger( 'jetpack-lazy-loaded-image' );
+ }
+};
+
+/**
+ * The following is an Intersection observer polyfill which is licensed under
+ * the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE and can be found at:
+ * https://github.com/w3c/IntersectionObserver/tree/master/polyfill
+ */
+
+/* jshint ignore:start */
+/**
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE.
+ *
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
+ *
+ */
+
+(function(window, document) {
+ 'use strict';
+
+
+ // Exits early if all IntersectionObserver and IntersectionObserverEntry
+ // features are natively supported.
+ if ('IntersectionObserver' in window &&
+ 'IntersectionObserverEntry' in window &&
+ 'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
+
+ // Minimal polyfill for Edge 15's lack of `isIntersecting`
+ // See: https://github.com/w3c/IntersectionObserver/issues/211
+ if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
+ Object.defineProperty(window.IntersectionObserverEntry.prototype,
+ 'isIntersecting', {
+ get: function () {
+ return this.intersectionRatio > 0;
+ }
+ });
+ }
+ return;
+ }
+
+
+ /**
+ * An IntersectionObserver registry. This registry exists to hold a strong
+ * reference to IntersectionObserver instances currently observering a target
+ * element. Without this registry, instances without another reference may be
+ * garbage collected.
+ */
+ var registry = [];
+
+
+ /**
+ * Creates the global IntersectionObserverEntry constructor.
+ * https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
+ * @param {Object} entry A dictionary of instance properties.
+ * @constructor
+ */
+ function IntersectionObserverEntry(entry) {
+ this.time = entry.time;
+ this.target = entry.target;
+ this.rootBounds = entry.rootBounds;
+ this.boundingClientRect = entry.boundingClientRect;
+ this.intersectionRect = entry.intersectionRect || getEmptyRect();
+ this.isIntersecting = !!entry.intersectionRect;
+
+ // Calculates the intersection ratio.
+ var targetRect = this.boundingClientRect;
+ var targetArea = targetRect.width * targetRect.height;
+ var intersectionRect = this.intersectionRect;
+ var intersectionArea = intersectionRect.width * intersectionRect.height;
+
+ // Sets intersection ratio.
+ if (targetArea) {
+ this.intersectionRatio = intersectionArea / targetArea;
+ } else {
+ // If area is zero and is intersecting, sets to 1, otherwise to 0
+ this.intersectionRatio = this.isIntersecting ? 1 : 0;
+ }
+ }
+
+
+ /**
+ * Creates the global IntersectionObserver constructor.
+ * https://w3c.github.io/IntersectionObserver/#intersection-observer-interface
+ * @param {Function} callback The function to be invoked after intersection
+ * changes have queued. The function is not invoked if the queue has
+ * been emptied by calling the `takeRecords` method.
+ * @param {Object=} opt_options Optional configuration options.
+ * @constructor
+ */
+ function IntersectionObserver(callback, opt_options) {
+
+ var options = opt_options || {};
+
+ if (typeof callback != 'function') {
+ throw new Error('callback must be a function');
+ }
+
+ if (options.root && options.root.nodeType != 1) {
+ throw new Error('root must be an Element');
+ }
+
+ // Binds and throttles `this._checkForIntersections`.
+ this._checkForIntersections = throttle(
+ this._checkForIntersections.bind(this), this.THROTTLE_TIMEOUT);
+
+ // Private properties.
+ this._callback = callback;
+ this._observationTargets = [];
+ this._queuedEntries = [];
+ this._rootMarginValues = this._parseRootMargin(options.rootMargin);
+
+ // Public properties.
+ this.thresholds = this._initThresholds(options.threshold);
+ this.root = options.root || null;
+ this.rootMargin = this._rootMarginValues.map(function(margin) {
+ return margin.value + margin.unit;
+ }).join(' ');
+ }
+
+
+ /**
+ * The minimum interval within which the document will be checked for
+ * intersection changes.
+ */
+ IntersectionObserver.prototype.THROTTLE_TIMEOUT = 100;
+
+
+ /**
+ * The frequency in which the polyfill polls for intersection changes.
+ * this can be updated on a per instance basis and must be set prior to
+ * calling `observe` on the first target.
+ */
+ IntersectionObserver.prototype.POLL_INTERVAL = null;
+
+ /**
+ * Use a mutation observer on the root element
+ * to detect intersection changes.
+ */
+ IntersectionObserver.prototype.USE_MUTATION_OBSERVER = true;
+
+
+ /**
+ * Starts observing a target element for intersection changes based on
+ * the thresholds values.
+ * @param {Element} target The DOM element to observe.
+ */
+ IntersectionObserver.prototype.observe = function(target) {
+ var isTargetAlreadyObserved = this._observationTargets.some(function(item) {
+ return item.element == target;
+ });
+
+ if (isTargetAlreadyObserved) {
+ return;
+ }
+
+ if (!(target && target.nodeType == 1)) {
+ throw new Error('target must be an Element');
+ }
+
+ this._registerInstance();
+ this._observationTargets.push({element: target, entry: null});
+ this._monitorIntersections();
+ this._checkForIntersections();
+ };
+
+
+ /**
+ * Stops observing a target element for intersection changes.
+ * @param {Element} target The DOM element to observe.
+ */
+ IntersectionObserver.prototype.unobserve = function(target) {
+ this._observationTargets =
+ this._observationTargets.filter(function(item) {
+
+ return item.element != target;
+ });
+ if (!this._observationTargets.length) {
+ this._unmonitorIntersections();
+ this._unregisterInstance();
+ }
+ };
+
+
+ /**
+ * Stops observing all target elements for intersection changes.
+ */
+ IntersectionObserver.prototype.disconnect = function() {
+ this._observationTargets = [];
+ this._unmonitorIntersections();
+ this._unregisterInstance();
+ };
+
+
+ /**
+ * Returns any queue entries that have not yet been reported to the
+ * callback and clears the queue. This can be used in conjunction with the
+ * callback to obtain the absolute most up-to-date intersection information.
+ * @return {Array} The currently queued entries.
+ */
+ IntersectionObserver.prototype.takeRecords = function() {
+ var records = this._queuedEntries.slice();
+ this._queuedEntries = [];
+ return records;
+ };
+
+
+ /**
+ * Accepts the threshold value from the user configuration object and
+ * returns a sorted array of unique threshold values. If a value is not
+ * between 0 and 1 and error is thrown.
+ * @private
+ * @param {Array|number=} opt_threshold An optional threshold value or
+ * a list of threshold values, defaulting to [0].
+ * @return {Array} A sorted list of unique and valid threshold values.
+ */
+ IntersectionObserver.prototype._initThresholds = function(opt_threshold) {
+ var threshold = opt_threshold || [0];
+ if (!Array.isArray(threshold)) threshold = [threshold];
+
+ return threshold.sort().filter(function(t, i, a) {
+ if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) {
+ throw new Error('threshold must be a number between 0 and 1 inclusively');
+ }
+ return t !== a[i - 1];
+ });
+ };
+
+
+ /**
+ * Accepts the rootMargin value from the user configuration object
+ * and returns an array of the four margin values as an object containing
+ * the value and unit properties. If any of the values are not properly
+ * formatted or use a unit other than px or %, and error is thrown.
+ * @private
+ * @param {string=} opt_rootMargin An optional rootMargin value,
+ * defaulting to '0px'.
+ * @return {Array<Object>} An array of margin objects with the keys
+ * value and unit.
+ */
+ IntersectionObserver.prototype._parseRootMargin = function(opt_rootMargin) {
+ var marginString = opt_rootMargin || '0px';
+ var margins = marginString.split(/\s+/).map(function(margin) {
+ var parts = /^(-?\d*\.?\d+)(px|%)$/.exec(margin);
+ if (!parts) {
+ throw new Error('rootMargin must be specified in pixels or percent');
+ }
+ return {value: parseFloat(parts[1]), unit: parts[2]};
+ });
+
+ // Handles shorthand.
+ margins[1] = margins[1] || margins[0];
+ margins[2] = margins[2] || margins[0];
+ margins[3] = margins[3] || margins[1];
+
+ return margins;
+ };
+
+
+ /**
+ * Starts polling for intersection changes if the polling is not already
+ * happening, and if the page's visibilty state is visible.
+ * @private
+ */
+ IntersectionObserver.prototype._monitorIntersections = function() {
+ if (!this._monitoringIntersections) {
+ this._monitoringIntersections = true;
+
+ // If a poll interval is set, use polling instead of listening to
+ // resize and scroll events or DOM mutations.
+ if (this.POLL_INTERVAL) {
+ this._monitoringInterval = setInterval(
+ this._checkForIntersections, this.POLL_INTERVAL);
+ }
+ else {
+ addEvent(window, 'resize', this._checkForIntersections, true);
+ addEvent(document, 'scroll', this._checkForIntersections, true);
+
+ if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in window) {
+ this._domObserver = new MutationObserver(this._checkForIntersections);
+ this._domObserver.observe(document, {
+ attributes: true,
+ childList: true,
+ characterData: true,
+ subtree: true
+ });
+ }
+ }
+ }
+ };
+
+
+ /**
+ * Stops polling for intersection changes.
+ * @private
+ */
+ IntersectionObserver.prototype._unmonitorIntersections = function() {
+ if (this._monitoringIntersections) {
+ this._monitoringIntersections = false;
+
+ clearInterval(this._monitoringInterval);
+ this._monitoringInterval = null;
+
+ removeEvent(window, 'resize', this._checkForIntersections, true);
+ removeEvent(document, 'scroll', this._checkForIntersections, true);
+
+ if (this._domObserver) {
+ this._domObserver.disconnect();
+ this._domObserver = null;
+ }
+ }
+ };
+
+
+ /**
+ * Scans each observation target for intersection changes and adds them
+ * to the internal entries queue. If new entries are found, it
+ * schedules the callback to be invoked.
+ * @private
+ */
+ IntersectionObserver.prototype._checkForIntersections = function() {
+ var rootIsInDom = this._rootIsInDom();
+ var rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect();
+
+ this._observationTargets.forEach(function(item) {
+ var target = item.element;
+ var targetRect = getBoundingClientRect(target);
+ var rootContainsTarget = this._rootContainsTarget(target);
+ var oldEntry = item.entry;
+ var intersectionRect = rootIsInDom && rootContainsTarget &&
+ this._computeTargetAndRootIntersection(target, rootRect);
+
+ var newEntry = item.entry = new IntersectionObserverEntry({
+ time: now(),
+ target: target,
+ boundingClientRect: targetRect,
+ rootBounds: rootRect,
+ intersectionRect: intersectionRect
+ });
+
+ if (!oldEntry) {
+ this._queuedEntries.push(newEntry);
+ } else if (rootIsInDom && rootContainsTarget) {
+ // If the new entry intersection ratio has crossed any of the
+ // thresholds, add a new entry.
+ if (this._hasCrossedThreshold(oldEntry, newEntry)) {
+ this._queuedEntries.push(newEntry);
+ }
+ } else {
+ // If the root is not in the DOM or target is not contained within
+ // root but the previous entry for this target had an intersection,
+ // add a new record indicating removal.
+ if (oldEntry && oldEntry.isIntersecting) {
+ this._queuedEntries.push(newEntry);
+ }
+ }
+ }, this);
+
+ if (this._queuedEntries.length) {
+ this._callback(this.takeRecords(), this);
+ }
+ };
+
+
+ /**
+ * Accepts a target and root rect computes the intersection between then
+ * following the algorithm in the spec.
+ * TODO(philipwalton): at this time clip-path is not considered.
+ * https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo
+ * @param {Element} target The target DOM element
+ * @param {Object} rootRect The bounding rect of the root after being
+ * expanded by the rootMargin value.
+ * @return {?Object} The final intersection rect object or undefined if no
+ * intersection is found.
+ * @private
+ */
+ IntersectionObserver.prototype._computeTargetAndRootIntersection =
+ function(target, rootRect) {
+
+ // If the element isn't displayed, an intersection can't happen.
+ if (window.getComputedStyle(target).display == 'none') return;
+
+ var targetRect = getBoundingClientRect(target);
+ var intersectionRect = targetRect;
+ var parent = getParentNode(target);
+ var atRoot = false;
+
+ while (!atRoot) {
+ var parentRect = null;
+ var parentComputedStyle = parent.nodeType == 1 ?
+ window.getComputedStyle(parent) : {};
+
+ // If the parent isn't displayed, an intersection can't happen.
+ if (parentComputedStyle.display == 'none') return;
+
+ if (parent == this.root || parent == document) {
+ atRoot = true;
+ parentRect = rootRect;
+ } else {
+ // If the element has a non-visible overflow, and it's not the <body>
+ // or <html> element, update the intersection rect.
+ // Note: <body> and <html> cannot be clipped to a rect that's not also
+ // the document rect, so no need to compute a new intersection.
+ if (parent != document.body &&
+ parent != document.documentElement &&
+ parentComputedStyle.overflow != 'visible') {
+ parentRect = getBoundingClientRect(parent);
+ }
+ }
+
+ // If either of the above conditionals set a new parentRect,
+ // calculate new intersection data.
+ if (parentRect) {
+ intersectionRect = computeRectIntersection(parentRect, intersectionRect);
+
+ if (!intersectionRect) break;
+ }
+ parent = getParentNode(parent);
+ }
+ return intersectionRect;
+ };
+
+
+ /**
+ * Returns the root rect after being expanded by the rootMargin value.
+ * @return {Object} The expanded root rect.
+ * @private
+ */
+ IntersectionObserver.prototype._getRootRect = function() {
+ var rootRect;
+ if (this.root) {
+ rootRect = getBoundingClientRect(this.root);
+ } else {
+ // Use <html>/<body> instead of window since scroll bars affect size.
+ var html = document.documentElement;
+ var body = document.body;
+ rootRect = {
+ top: 0,
+ left: 0,
+ right: html.clientWidth || body.clientWidth,
+ width: html.clientWidth || body.clientWidth,
+ bottom: html.clientHeight || body.clientHeight,
+ height: html.clientHeight || body.clientHeight
+ };
+ }
+ return this._expandRectByRootMargin(rootRect);
+ };
+
+
+ /**
+ * Accepts a rect and expands it by the rootMargin value.
+ * @param {Object} rect The rect object to expand.
+ * @return {Object} The expanded rect.
+ * @private
+ */
+ IntersectionObserver.prototype._expandRectByRootMargin = function(rect) {
+ var margins = this._rootMarginValues.map(function(margin, i) {
+ return margin.unit == 'px' ? margin.value :
+ margin.value * (i % 2 ? rect.width : rect.height) / 100;
+ });
+ var newRect = {
+ top: rect.top - margins[0],
+ right: rect.right + margins[1],
+ bottom: rect.bottom + margins[2],
+ left: rect.left - margins[3]
+ };
+ newRect.width = newRect.right - newRect.left;
+ newRect.height = newRect.bottom - newRect.top;
+
+ return newRect;
+ };
+
+
+ /**
+ * Accepts an old and new entry and returns true if at least one of the
+ * threshold values has been crossed.
+ * @param {?IntersectionObserverEntry} oldEntry The previous entry for a
+ * particular target element or null if no previous entry exists.
+ * @param {IntersectionObserverEntry} newEntry The current entry for a
+ * particular target element.
+ * @return {boolean} Returns true if a any threshold has been crossed.
+ * @private
+ */
+ IntersectionObserver.prototype._hasCrossedThreshold =
+ function(oldEntry, newEntry) {
+
+ // To make comparing easier, an entry that has a ratio of 0
+ // but does not actually intersect is given a value of -1
+ var oldRatio = oldEntry && oldEntry.isIntersecting ?
+ oldEntry.intersectionRatio || 0 : -1;
+ var newRatio = newEntry.isIntersecting ?
+ newEntry.intersectionRatio || 0 : -1;
+
+ // Ignore unchanged ratios
+ if (oldRatio === newRatio) return;
+
+ for (var i = 0; i < this.thresholds.length; i++) {
+ var threshold = this.thresholds[i];
+
+ // Return true if an entry matches a threshold or if the new ratio
+ // and the old ratio are on the opposite sides of a threshold.
+ if (threshold == oldRatio || threshold == newRatio ||
+ threshold < oldRatio !== threshold < newRatio) {
+ return true;
+ }
+ }
+ };
+
+
+ /**
+ * Returns whether or not the root element is an element and is in the DOM.
+ * @return {boolean} True if the root element is an element and is in the DOM.
+ * @private
+ */
+ IntersectionObserver.prototype._rootIsInDom = function() {
+ return !this.root || containsDeep(document, this.root);
+ };
+
+
+ /**
+ * Returns whether or not the target element is a child of root.
+ * @param {Element} target The target element to check.
+ * @return {boolean} True if the target element is a child of root.
+ * @private
+ */
+ IntersectionObserver.prototype._rootContainsTarget = function(target) {
+ return containsDeep(this.root || document, target);
+ };
+
+
+ /**
+ * Adds the instance to the global IntersectionObserver registry if it isn't
+ * already present.
+ * @private
+ */
+ IntersectionObserver.prototype._registerInstance = function() {
+ if (registry.indexOf(this) < 0) {
+ registry.push(this);
+ }
+ };
+
+
+ /**
+ * Removes the instance from the global IntersectionObserver registry.
+ * @private
+ */
+ IntersectionObserver.prototype._unregisterInstance = function() {
+ var index = registry.indexOf(this);
+ if (index != -1) registry.splice(index, 1);
+ };
+
+
+ /**
+ * Returns the result of the performance.now() method or null in browsers
+ * that don't support the API.
+ * @return {number} The elapsed time since the page was requested.
+ */
+ function now() {
+ return window.performance && performance.now && performance.now();
+ }
+
+
+ /**
+ * Throttles a function and delays its executiong, so it's only called at most
+ * once within a given time period.
+ * @param {Function} fn The function to throttle.
+ * @param {number} timeout The amount of time that must pass before the
+ * function can be called again.
+ * @return {Function} The throttled function.
+ */
+ function throttle(fn, timeout) {
+ var timer = null;
+ return function () {
+ if (!timer) {
+ timer = setTimeout(function() {
+ fn();
+ timer = null;
+ }, timeout);
+ }
+ };
+ }
+
+
+ /**
+ * Adds an event handler to a DOM node ensuring cross-browser compatibility.
+ * @param {Node} node The DOM node to add the event handler to.
+ * @param {string} event The event name.
+ * @param {Function} fn The event handler to add.
+ * @param {boolean} opt_useCapture Optionally adds the even to the capture
+ * phase. Note: this only works in modern browsers.
+ */
+ function addEvent(node, event, fn, opt_useCapture) {
+ if (typeof node.addEventListener == 'function') {
+ node.addEventListener(event, fn, opt_useCapture || false);
+ }
+ else if (typeof node.attachEvent == 'function') {
+ node.attachEvent('on' + event, fn);
+ }
+ }
+
+
+ /**
+ * Removes a previously added event handler from a DOM node.
+ * @param {Node} node The DOM node to remove the event handler from.
+ * @param {string} event The event name.
+ * @param {Function} fn The event handler to remove.
+ * @param {boolean} opt_useCapture If the event handler was added with this
+ * flag set to true, it should be set to true here in order to remove it.
+ */
+ function removeEvent(node, event, fn, opt_useCapture) {
+ if (typeof node.removeEventListener == 'function') {
+ node.removeEventListener(event, fn, opt_useCapture || false);
+ }
+ else if (typeof node.detatchEvent == 'function') {
+ node.detatchEvent('on' + event, fn);
+ }
+ }
+
+
+ /**
+ * Returns the intersection between two rect objects.
+ * @param {Object} rect1 The first rect.
+ * @param {Object} rect2 The second rect.
+ * @return {?Object} The intersection rect or undefined if no intersection
+ * is found.
+ */
+ function computeRectIntersection(rect1, rect2) {
+ var top = Math.max(rect1.top, rect2.top);
+ var bottom = Math.min(rect1.bottom, rect2.bottom);
+ var left = Math.max(rect1.left, rect2.left);
+ var right = Math.min(rect1.right, rect2.right);
+ var width = right - left;
+ var height = bottom - top;
+
+ return (width >= 0 && height >= 0) && {
+ top: top,
+ bottom: bottom,
+ left: left,
+ right: right,
+ width: width,
+ height: height
+ };
+ }
+
+
+ /**
+ * Shims the native getBoundingClientRect for compatibility with older IE.
+ * @param {Element} el The element whose bounding rect to get.
+ * @return {Object} The (possibly shimmed) rect of the element.
+ */
+ function getBoundingClientRect(el) {
+ var rect;
+
+ try {
+ rect = el.getBoundingClientRect();
+ } catch (err) {
+ // Ignore Windows 7 IE11 "Unspecified error"
+ // https://github.com/w3c/IntersectionObserver/pull/205
+ }
+
+ if (!rect) return getEmptyRect();
+
+ // Older IE
+ if (!(rect.width && rect.height)) {
+ rect = {
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left,
+ width: rect.right - rect.left,
+ height: rect.bottom - rect.top
+ };
+ }
+ return rect;
+ }
+
+
+ /**
+ * Returns an empty rect object. An empty rect is returned when an element
+ * is not in the DOM.
+ * @return {Object} The empty rect.
+ */
+ function getEmptyRect() {
+ return {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ width: 0,
+ height: 0
+ };
+ }
+
+ /**
+ * Checks to see if a parent element contains a child elemnt (including inside
+ * shadow DOM).
+ * @param {Node} parent The parent element.
+ * @param {Node} child The child element.
+ * @return {boolean} True if the parent node contains the child node.
+ */
+ function containsDeep(parent, child) {
+ var node = child;
+ while (node) {
+ if (node == parent) return true;
+
+ node = getParentNode(node);
+ }
+ return false;
+ }
+
+
+ /**
+ * Gets the parent node of an element or its host element if the parent node
+ * is a shadow root.
+ * @param {Node} node The node whose parent to get.
+ * @return {Node|null} The parent node or null if no parent exists.
+ */
+ function getParentNode(node) {
+ var parent = node.parentNode;
+
+ if (parent && parent.nodeType == 11 && parent.host) {
+ // If the parent is a shadow root, return the host element.
+ return parent.host;
+ }
+ return parent;
+ }
+
+
+ // Exposes the constructors globally.
+ window.IntersectionObserver = IntersectionObserver;
+ window.IntersectionObserverEntry = IntersectionObserverEntry;
+
+ }(window, document));
+/* jshint ignore:end */
+
+// Let's kick things off now
+jetpackLazyImagesModule( jQuery );
diff --git a/plugins/jetpack/modules/lazy-images/lazy-images.php b/plugins/jetpack/modules/lazy-images/lazy-images.php
new file mode 100644
index 00000000..befa5ec8
--- /dev/null
+++ b/plugins/jetpack/modules/lazy-images/lazy-images.php
@@ -0,0 +1,353 @@
+<?php
+
+class Jetpack_Lazy_Images {
+ private static $__instance = null;
+
+ /**
+ * Singleton implementation
+ *
+ * @return object
+ */
+ public static function instance() {
+ if ( is_null( self::$__instance ) ) {
+ self::$__instance = new Jetpack_Lazy_Images();
+ }
+
+ return self::$__instance;
+ }
+
+ /**
+ * Registers actions
+ */
+ private function __construct() {
+ if ( is_admin() ) {
+ return;
+ }
+
+ /**
+ * Whether the lazy-images module should load.
+ *
+ * This filter is not prefixed with jetpack_ to provide a smoother migration
+ * process from the WordPress Lazy Load plugin.
+ *
+ * @module lazy-images
+ *
+ * @since 5.6.0
+ *
+ * @param bool true Whether lazy image loading should occur.
+ */
+ if ( ! apply_filters( 'lazyload_is_enabled', true ) ) {
+ return;
+ }
+
+ if ( Jetpack_AMP_Support::is_amp_request() ) {
+ return;
+ }
+
+ add_action( 'wp_head', array( $this, 'setup_filters' ), 9999 ); // we don't really want to modify anything in <head> since it's mostly all metadata
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
+
+ // Do not lazy load avatar in admin bar
+ add_action( 'admin_bar_menu', array( $this, 'remove_filters' ), 0 );
+
+ add_filter( 'wp_kses_allowed_html', array( $this, 'allow_lazy_attributes' ) );
+ add_action( 'wp_head', array( $this, 'add_nojs_fallback' ) );
+ }
+
+ public function setup_filters() {
+ add_filter( 'the_content', array( $this, 'add_image_placeholders' ), PHP_INT_MAX ); // run this later, so other content filters have run, including image_add_wh on WP.com
+ add_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
+ add_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
+ add_filter( 'widget_text', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
+ add_filter( 'get_image_tag', array( $this, 'add_image_placeholders' ), PHP_INT_MAX);
+ add_filter( 'wp_get_attachment_image_attributes', array( __CLASS__, 'process_image_attributes' ), PHP_INT_MAX );
+ }
+
+ public function remove_filters() {
+ remove_filter( 'the_content', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
+ remove_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
+ remove_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
+ remove_filter( 'widget_text', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
+ remove_filter( 'get_image_tag', array( $this, 'add_image_placeholders' ), PHP_INT_MAX);
+ remove_filter( 'wp_get_attachment_image_attributes', array( __CLASS__, 'process_image_attributes' ), PHP_INT_MAX );
+ }
+
+ /**
+ * Ensure that our lazy image attributes are not filtered out of image tags.
+ *
+ * @param array $allowed_tags The allowed tags and their attributes.
+ * @return array
+ */
+ public function allow_lazy_attributes( $allowed_tags ) {
+ if ( ! isset( $allowed_tags['img'] ) ) {
+ return $allowed_tags;
+ }
+
+ // But, if images are allowed, ensure that our attributes are allowed!
+ $img_attributes = array_merge( $allowed_tags['img'], array(
+ 'data-lazy-src' => 1,
+ 'data-lazy-srcset' => 1,
+ 'data-lazy-sizes' => 1,
+ ) );
+
+ $allowed_tags['img'] = $img_attributes;
+
+ return $allowed_tags;
+ }
+
+ public function add_image_placeholders( $content ) {
+ // Don't lazyload for feeds, previews
+ if ( is_feed() || is_preview() ) {
+ return $content;
+ }
+
+ // Don't lazy-load if the content has already been run through previously
+ if ( false !== strpos( $content, 'data-lazy-src' ) ) {
+ return $content;
+ }
+
+ // This is a pretty simple regex, but it works
+ $content = preg_replace_callback( '#<(img)([^>]+?)(>(.*?)</\\1>|[\/]?>)#si', array( __CLASS__, 'process_image' ), $content );
+
+ return $content;
+ }
+
+ /**
+ * Returns true when a given string of classes contains a class signifying lazy images
+ * should not process the image.
+ *
+ * @since 5.9.0
+ *
+ * @param string $classes A string of space-separated classes.
+ * @return bool
+ */
+ public static function should_skip_image_with_blacklisted_class( $classes ) {
+ $blacklisted_classes = array(
+ 'skip-lazy',
+ 'gazette-featured-content-thumbnail',
+ );
+
+ /**
+ * Allow plugins and themes to tell lazy images to skip an image with a given class.
+ *
+ * @module lazy-images
+ *
+ * @since 5.9.0
+ *
+ * @param array An array of strings where each string is a class.
+ */
+ $blacklisted_classes = apply_filters( 'jetpack_lazy_images_blacklisted_classes', $blacklisted_classes );
+
+ if ( ! is_array( $blacklisted_classes ) || empty( $blacklisted_classes ) ) {
+ return false;
+ }
+
+ foreach ( $blacklisted_classes as $class ) {
+ if ( false !== strpos( $classes, $class ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Processes images in content by acting as the preg_replace_callback
+ *
+ * @since 5.6.0
+ *
+ * @param array $matches
+ *
+ * @return string The image with updated lazy attributes
+ */
+ static function process_image( $matches ) {
+ $old_attributes_str = $matches[2];
+ $old_attributes_kses_hair = wp_kses_hair( $old_attributes_str, wp_allowed_protocols() );
+
+ if ( empty( $old_attributes_kses_hair['src'] ) ) {
+ return $matches[0];
+ }
+
+ $old_attributes = self::flatten_kses_hair_data( $old_attributes_kses_hair );
+
+ // If we didn't add lazy attributes, just return the original image source.
+ if ( ! empty( $old_attributes['class'] ) && false !== strpos( $old_attributes['class'], 'jetpack-lazy-image' ) ) {
+ return $matches[0];
+ }
+
+ $new_attributes = self::process_image_attributes( $old_attributes );
+ $new_attributes_str = self::build_attributes_string( $new_attributes );
+
+ return sprintf( '<img %1$s><noscript>%2$s</noscript>', $new_attributes_str, $matches[0] );
+ }
+
+ /**
+ * Given an array of image attributes, updates the `src`, `srcset`, and `sizes` attributes so
+ * that they load lazily.
+ *
+ * @since 5.7.0
+ *
+ * @param array $attributes
+ *
+ * @return array The updated image attributes array with lazy load attributes
+ */
+ static function process_image_attributes( $attributes ) {
+ if ( empty( $attributes['src'] ) ) {
+ return $attributes;
+ }
+
+ if ( ! empty( $attributes['class'] ) && self::should_skip_image_with_blacklisted_class( $attributes['class'] ) ) {
+ return $attributes;
+ }
+
+ /**
+ * Allow plugins and themes to conditionally skip processing an image via its attributes.
+ *
+ * @module-lazy-images
+ *
+ * @deprecated 6.5.0 Use jetpack_lazy_images_skip_image_with_attributes instead.
+ *
+ * @since 5.9.0
+ *
+ * @param bool Default to not skip processing the current image.
+ * @param array An array of attributes via wp_kses_hair() for the current image.
+ */
+ if ( apply_filters( 'jetpack_lazy_images_skip_image_with_atttributes', false, $attributes ) ) {
+ return $attributes;
+ }
+
+ /**
+ * Allow plugins and themes to conditionally skip processing an image via its attributes.
+ *
+ * @module-lazy-images
+ *
+ * @since 6.5.0 Filter name was updated from jetpack_lazy_images_skip_image_with_atttributes to correct typo.
+ * @since 5.9.0
+ *
+ * @param bool Default to not skip processing the current image.
+ * @param array An array of attributes via wp_kses_hair() for the current image.
+ */
+ if ( apply_filters( 'jetpack_lazy_images_skip_image_with_attributes', false, $attributes ) ) {
+ return $attributes;
+ }
+
+ $old_attributes = $attributes;
+
+ // Stash srcset and sizes in data attributes.
+ foreach ( array( 'srcset', 'sizes' ) as $attribute ) {
+ if ( isset( $old_attributes[ $attribute ] ) ) {
+ $attributes[ "data-lazy-$attribute" ] = $old_attributes[ $attribute ];
+ unset( $attributes[ $attribute ] );
+ }
+ }
+
+ // We set this, adding the query arg so that it doesn't exactly equal the src attribute, so that photon JavaScript
+ // will hold off on processing this image.
+ $attributes['data-lazy-src'] = esc_url_raw( add_query_arg( 'is-pending-load', true, $attributes['src'] ) );
+
+ $attributes['srcset'] = self::get_placeholder_image();
+ $attributes['class'] = sprintf(
+ '%s jetpack-lazy-image',
+ empty( $old_attributes['class'] )
+ ? ''
+ : $old_attributes['class']
+ );
+
+ /**
+ * Allow plugins and themes to override the attributes on the image before the content is updated.
+ *
+ * One potential use of this filter is for themes that set `height:auto` on the `img` tag.
+ * With this filter, the theme could get the width and height attributes from the
+ * $attributes array and then add a style tag that sets those values as well, which could
+ * minimize reflow as images load.
+ *
+ * @module lazy-images
+ *
+ * @since 5.6.0
+ *
+ * @param array An array containing the attributes for the image, where the key is the attribute name
+ * and the value is the attribute value.
+ */
+ return apply_filters( 'jetpack_lazy_images_new_attributes', $attributes );
+ }
+
+ /**
+ * Adds JavaScript to check if the current browser supports JavaScript as well as some styles to hide lazy
+ * images when the browser does not support JavaScript.
+ *
+ * @return void
+ */
+ public function add_nojs_fallback() {
+ ?>
+ <style type="text/css">
+ /* If html does not have either class, do not show lazy loaded images. */
+ html:not( .jetpack-lazy-images-js-enabled ):not( .js ) .jetpack-lazy-image {
+ display: none;
+ }
+ </style>
+ <script>
+ document.documentElement.classList.add(
+ 'jetpack-lazy-images-js-enabled'
+ );
+ </script>
+ <?php
+ }
+
+ /**
+ * Retrieves the placeholder image after running it through the lazyload_images_placeholder_image filter.
+ *
+ * @return string The placeholder image source.
+ */
+ private static function get_placeholder_image() {
+ /**
+ * Allows plugins and themes to modify the placeholder image.
+ *
+ * This filter is not prefixed with jetpack_ to provide a smoother migration
+ * process from the WordPress Lazy Load plugin.
+ *
+ * @module lazy-images
+ *
+ * @since 5.6.0
+ * @since 6.5.0 Default image is now a base64 encoded transparent gif.
+ *
+ * @param string The URL to the placeholder image
+ */
+ return apply_filters(
+ 'lazyload_images_placeholder_image',
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
+ );
+ }
+
+ private static function flatten_kses_hair_data( $attributes ) {
+ $flattened_attributes = array();
+ foreach ( $attributes as $name => $attribute ) {
+ $flattened_attributes[ $name ] = $attribute['value'];
+ }
+ return $flattened_attributes;
+ }
+
+ private static function build_attributes_string( $attributes ) {
+ $string = array();
+ foreach ( $attributes as $name => $value ) {
+ if ( '' === $value ) {
+ $string[] = sprintf( '%s', $name );
+ } else {
+ $string[] = sprintf( '%s="%s"', $name, esc_attr( $value ) );
+ }
+ }
+ return implode( ' ', $string );
+ }
+
+ public function enqueue_assets() {
+ wp_enqueue_script(
+ 'jetpack-lazy-images',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/lazy-images/js/lazy-images.min.js',
+ 'modules/lazy-images/js/lazy-images.js'
+ ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ true
+ );
+ }
+}
diff --git a/plugins/jetpack/modules/likes.php b/plugins/jetpack/modules/likes.php
new file mode 100644
index 00000000..efc264d7
--- /dev/null
+++ b/plugins/jetpack/modules/likes.php
@@ -0,0 +1,657 @@
+<?php
+/**
+ * Module Name: Likes
+ * Module Description: Give visitors an easy way to show they appreciate your content.
+ * First Introduced: 2.2
+ * Sort Order: 23
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Social
+ * Feature: Engagement
+ * Additional Search Queries: like, likes, wordpress.com
+ */
+
+Jetpack::dns_prefetch( array(
+ '//widgets.wp.com',
+ '//s0.wp.com',
+ '//0.gravatar.com',
+ '//1.gravatar.com',
+ '//2.gravatar.com',
+) );
+
+include_once dirname( __FILE__ ) . '/likes/jetpack-likes-master-iframe.php';
+include_once dirname( __FILE__ ) . '/likes/jetpack-likes-settings.php';
+
+class Jetpack_Likes {
+ public static function init() {
+ static $instance = NULL;
+
+ if ( ! $instance ) {
+ $instance = new Jetpack_Likes;
+ }
+
+ return $instance;
+ }
+
+ function __construct() {
+ $this->in_jetpack = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ? false : true;
+ $this->settings = new Jetpack_Likes_Settings();
+
+ // We need to run on wp hook rather than init because we check is_amp_endpoint()
+ // when bootstrapping hooks
+ add_action( 'wp', array( &$this, 'action_init' ), 99 );
+
+ add_action( 'admin_init', array( $this, 'admin_init' ) );
+
+ if ( $this->in_jetpack ) {
+ add_action( 'jetpack_activate_module_likes', array( $this, 'set_social_notifications_like' ) );
+ add_action( 'jetpack_deactivate_module_likes', array( $this, 'delete_social_notifications_like' ) );
+
+ Jetpack::enable_module_configurable( __FILE__ );
+ add_filter( 'jetpack_module_configuration_url_likes', array( $this, 'jetpack_likes_configuration_url' ) );
+ add_action( 'admin_print_scripts-settings_page_sharing', array( &$this, 'load_jp_css' ) );
+ add_filter( 'sharing_show_buttons_on_row_start', array( $this, 'configuration_target_area' ) );
+
+ $active = Jetpack::get_active_modules();
+
+ if ( ! in_array( 'sharedaddy', $active ) && ! in_array( 'publicize', $active ) ) {
+ // we don't have a sharing page yet
+ add_action( 'admin_menu', array( $this->settings, 'sharing_menu' ) );
+ }
+
+ if ( in_array( 'publicize', $active ) && ! in_array( 'sharedaddy', $active ) ) {
+ // we have a sharing page but not the global options area
+ add_action( 'pre_admin_screen_sharing', array( $this->settings, 'sharing_block' ), 20 );
+ add_action( 'pre_admin_screen_sharing', array( $this->settings, 'updated_message' ), -10 );
+ }
+
+ if( ! in_array( 'sharedaddy', $active ) ) {
+ add_action( 'admin_init', array( $this->settings, 'process_update_requests_if_sharedaddy_not_loaded' ) );
+ add_action( 'sharing_global_options', array( $this->settings, 'admin_settings_showbuttonon_init' ), 19 );
+ add_action( 'sharing_admin_update', array( $this->settings, 'admin_settings_showbuttonon_callback' ), 19 );
+ add_action( 'admin_init', array( $this->settings, 'add_meta_box' ) );
+ } else {
+ add_filter( 'sharing_meta_box_title', array( $this->settings, 'add_likes_to_sharing_meta_box_title' ) );
+ add_action( 'start_sharing_meta_box_content', array( $this->settings, 'meta_box_content' ) );
+ }
+ } else { // wpcom
+ add_action( 'wpmu_new_blog', array( $this, 'enable_comment_likes' ), 10, 1 );
+ add_action( 'admin_init', array( $this->settings, 'add_meta_box' ) );
+ add_action( 'end_likes_meta_box_content', array( $this->settings, 'sharing_meta_box_content' ) );
+ add_filter( 'likes_meta_box_title', array( $this->settings, 'add_likes_to_sharing_meta_box_title' ) );
+ }
+
+ add_action( 'admin_init', array( $this, 'admin_discussion_likes_settings_init' ) ); // Likes notifications
+
+ add_action( 'admin_bar_menu', array( $this, 'admin_bar_likes' ), 60 );
+
+ add_action( 'wp_enqueue_scripts', array( $this, 'load_styles_register_scripts' ) );
+
+ add_action( 'save_post', array( $this->settings, 'meta_box_save' ) );
+ add_action( 'edit_attachment', array( $this->settings, 'meta_box_save' ) );
+ add_action( 'sharing_global_options', array( $this->settings, 'admin_settings_init' ), 20 );
+ add_action( 'sharing_admin_update', array( $this->settings, 'admin_settings_callback' ), 20 );
+ }
+
+ /**
+ * Set the social_notifications_like option to `on` when the Likes module is activated.
+ *
+ * @since 3.7.0
+ *
+ * @return null
+ */
+ function set_social_notifications_like() {
+ update_option( 'social_notifications_like', 'on' );
+ }
+
+ /**
+ * Delete the social_notifications_like option that was set to `on` on module activation.
+ *
+ * @since 3.7.0
+ *
+ * @return null
+ */
+ function delete_social_notifications_like() {
+ delete_option( 'social_notifications_like' );
+ }
+
+
+ /**
+ * Overrides default configuration url
+ *
+ * @uses admin_url
+ * @return string module settings URL
+ */
+ function jetpack_likes_configuration_url() {
+ return admin_url( 'options-general.php?page=sharing#likes' );
+ }
+
+ /**
+ * Loads Jetpack's CSS on the sharing page so we can use .jetpack-targetable
+ */
+ function load_jp_css() {
+ // Do we really need `admin_styles`? With the new admin UI, it's breaking some bits.
+ // Jetpack::init()->admin_styles();
+ }
+
+ /**
+ * Load scripts and styles for front end.
+ * @return null
+ */
+ function load_styles_register_scripts() {
+ if ( $this->in_jetpack ) {
+ wp_enqueue_style( 'jetpack_likes', plugins_url( 'likes/style.css', __FILE__ ), array(), JETPACK__VERSION );
+ $this->register_scripts();
+ }
+ }
+
+
+ /**
+ * Stub for is_post_likeable, since some wpcom functions call this directly on the class
+ * Are likes enabled for this post?
+ *
+ * @param int $post_id
+ * @return bool
+ */
+ static function is_post_likeable( $post_id = 0 ) {
+ _deprecated_function( __METHOD__, 'jetpack-5.4', 'Jetpack_Likes_Settings()->is_post_likeable' );
+ $settings = new Jetpack_Likes_Settings();
+ return $settings->is_post_likeable();
+ }
+
+ /**
+ * Stub for is_likes_visible, since some themes were calling it directly from this class
+ *
+ * @deprecated 5.4
+ * @return bool
+ */
+ function is_likes_visible() {
+ _deprecated_function( __METHOD__, 'jetpack-5.4', 'Jetpack_Likes_Settings()->is_likes_visible' );
+
+ $settings = new Jetpack_Likes_Settings();
+ return $settings->is_likes_visible();
+ }
+
+ /**
+ * Adds in the jetpack-targetable class so when we visit sharing#likes our like settings get highlighted by a yellow box
+ * @param string $html row heading for the sharedaddy "which page" setting
+ * @return string html with the jetpack-targetable class and likes id. tbody gets closed after the like settings
+ */
+ function configuration_target_area( $html = '' ) {
+ $html = "<tbody id='likes' class='jetpack-targetable'>" . $html;
+ return $html;
+ }
+
+ /**
+ * Options to be added to the discussion page (see also admin_settings_init, etc below for Sharing settings page)
+ */
+
+ function admin_discussion_likes_settings_init() {
+ // Add a temporary section, until we can move the setting out of there and with the rest of the email notification settings
+ add_settings_section( 'likes-notifications', __( 'Likes Notifications', 'jetpack' ), array( $this, 'admin_discussion_likes_settings_section' ), 'discussion' );
+ add_settings_field( 'social-notifications', __( 'Email me whenever', 'jetpack' ), array( $this, 'admin_discussion_likes_settings_field' ), 'discussion', 'likes-notifications' );
+ // Register the setting
+ register_setting( 'discussion', 'social_notifications_like', array( $this, 'admin_discussion_likes_settings_validate' ) );
+ }
+
+ function admin_discussion_likes_settings_section() {
+ // Atypical usage here. We emit jquery to move likes notification checkbox to be with the rest of the email notification settings
+?>
+ <script type="text/javascript">
+ jQuery( function( $ ) {
+ var table = $( '#social_notifications_like' ).parents( 'table:first' ),
+ header = table.prevAll( 'h2:first' ),
+ newParent = $( '#moderation_notify' ).parent( 'label' ).parent();
+
+ if ( !table.length || !header.length || !newParent.length ) {
+ return;
+ }
+
+ newParent.append( '<br/>' ).append( table.end().parent( 'label' ).siblings().andSelf() );
+ header.remove();
+ table.remove();
+ } );
+ </script>
+<?php
+ }
+
+ function admin_likes_get_option( $option ) {
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $option_setting = get_blog_option( get_current_blog_id(), $option, 'on' );
+ } else {
+ $option_setting = get_option( $option, 'on' );
+ }
+
+ return intval( 'on' == $option_setting );
+ }
+
+ function admin_discussion_likes_settings_field() {
+ $like = $this->admin_likes_get_option( 'social_notifications_like' );
+?>
+ <label><input type="checkbox" id="social_notifications_like" name="social_notifications_like" value="1" <?php checked( $like ); ?> /> <?php esc_html_e( 'Someone likes one of my posts', 'jetpack' ); ?></label>
+<?php
+ }
+
+ function admin_discussion_likes_settings_validate( $input ) {
+ // If it's not set (was unchecked during form submission) or was set to off (during option update), return 'off'.
+ if ( !$input || 'off' == $input )
+ return 'off';
+
+ // Otherwise, return 'on'.
+ return 'on';
+ }
+
+ function admin_init() {
+ add_filter( 'manage_posts_columns', array( $this, 'add_like_count_column' ) );
+ add_filter( 'manage_pages_columns', array( $this, 'add_like_count_column' ) );
+ add_action( 'manage_posts_custom_column', array( $this, 'likes_edit_column' ), 10, 2 );
+ add_action( 'manage_pages_custom_column', array( $this, 'likes_edit_column' ), 10, 2 );
+ add_action( 'admin_print_styles-edit.php', array( $this, 'load_admin_css' ) );
+ add_action( "admin_print_scripts-edit.php", array( $this, 'enqueue_admin_scripts' ) );
+ }
+
+ function action_init() {
+ if ( is_admin() ) {
+ return;
+ }
+
+ if ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) ||
+ ( defined( 'APP_REQUEST' ) && APP_REQUEST ) ||
+ ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) ||
+ ( defined( 'COOKIE_AUTH_REQUEST' ) && COOKIE_AUTH_REQUEST ) ||
+ ( defined( 'JABBER_SERVER' ) && JABBER_SERVER ) ) {
+ return;
+ }
+
+ if (
+ class_exists( 'Jetpack_AMP_Support' )
+ && Jetpack_AMP_Support::is_amp_request()
+ ) {
+ return;
+ }
+
+ if ( $this->in_jetpack ) {
+ add_filter( 'the_content', array( &$this, 'post_likes' ), 30, 1 );
+ add_filter( 'the_excerpt', array( &$this, 'post_likes' ), 30, 1 );
+
+ } else {
+ add_filter( 'post_flair', array( &$this, 'post_likes' ), 30, 1 );
+ add_filter( 'post_flair_block_css', array( $this, 'post_flair_service_enabled_like' ) );
+
+ wp_enqueue_script( 'postmessage', '/wp-content/js/postmessage.js', array( 'jquery' ), JETPACK__VERSION, false );
+ wp_enqueue_script( 'jetpack_resize', '/wp-content/js/jquery/jquery.jetpack-resize.js', array( 'jquery' ), JETPACK__VERSION, false );
+ wp_enqueue_script( 'jetpack_likes_queuehandler', plugins_url( 'queuehandler.js' , __FILE__ ), array( 'jquery', 'postmessage', 'jetpack_resize' ), JETPACK__VERSION, true );
+ wp_enqueue_style( 'jetpack_likes', plugins_url( 'jetpack-likes.css', __FILE__ ), array(), JETPACK__VERSION );
+ }
+ }
+
+ /**
+ * Register scripts
+ */
+ function register_scripts() {
+ wp_register_script(
+ 'postmessage',
+ Jetpack::get_file_url_for_environment( '_inc/build/postmessage.min.js', '_inc/postmessage.js' ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ false
+ );
+ wp_register_script(
+ 'jetpack_resize',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/jquery.jetpack-resize.min.js',
+ '_inc/jquery.jetpack-resize.js'
+ ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ false
+ );
+ wp_register_script(
+ 'jetpack_likes_queuehandler',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/likes/queuehandler.min.js',
+ 'modules/likes/queuehandler.js'
+ ),
+ array( 'jquery', 'postmessage', 'jetpack_resize' ),
+ JETPACK__VERSION,
+ true
+ );
+ }
+
+ /**
+ * Load the CSS needed for the wp-admin area.
+ */
+ function load_admin_css() {
+ ?>
+ <style type="text/css">
+ .vers img { display: none; }
+ .metabox-prefs .vers img { display: inline; }
+ .fixed .column-likes { width: 5.5em; padding: 8px 0; text-align: left; }
+ .fixed .column-stats { width: 5em; }
+ .fixed .column-likes .post-com-count {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ display: inline-block;
+ padding: 0 8px;
+ height: 2em;
+ margin-top: 5px;
+ -webkit-border-radius: 5px;
+ border-radius: 5px;
+ background-color: #72777C;
+ color: #FFF;
+ font-size: 11px;
+ line-height: 21px;
+ }
+ .fixed .column-likes .post-com-count::after { border: none !important; }
+ .fixed .column-likes .post-com-count:hover { background-color: #0073AA; }
+ .fixed .column-likes .vers:before {
+ font: normal 20px/1 dashicons;
+ content: '\f155';
+ speak: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ @media screen and (max-width: 782px) {
+ .fixed .column-likes {
+ display: none;
+ }
+ }
+ </style>
+ <?php
+ }
+
+ /**
+ * Load the JS required for loading the like counts.
+ */
+ function enqueue_admin_scripts() {
+ if ( empty( $_GET['post_type'] ) || 'post' == $_GET['post_type'] || 'page' == $_GET['post_type'] ) {
+ if ( $this->in_jetpack ) {
+ wp_enqueue_script(
+ 'likes-post-count',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/likes/post-count.min.js',
+ 'modules/likes/post-count.js'
+ ),
+ array( 'jquery' ),
+ JETPACK__VERSION
+ );
+ wp_enqueue_script(
+ 'likes-post-count-jetpack',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/likes/post-count-jetpack.min.js',
+ 'modules/likes/post-count-jetpack.js'
+ ),
+ array( 'likes-post-count' ),
+ JETPACK__VERSION
+ );
+ } else {
+ wp_enqueue_script( 'jquery.wpcom-proxy-request', "/wp-content/js/jquery/jquery.wpcom-proxy-request.js", array('jquery'), NULL, true );
+ wp_enqueue_script( 'likes-post-count', plugins_url( 'likes/post-count.js', dirname( __FILE__ ) ), array( 'jquery' ), JETPACK__VERSION );
+ wp_enqueue_script( 'likes-post-count-wpcom', plugins_url( 'likes/post-count-wpcom.js', dirname( __FILE__ ) ), array( 'likes-post-count', 'jquery.wpcom-proxy-request' ), JETPACK__VERSION );
+ }
+ }
+ }
+
+ /**
+ * Add "Likes" column data to the post edit table in wp-admin.
+ *
+ * @param string $column_name
+ * @param int $post_id
+ */
+ function likes_edit_column( $column_name, $post_id ) {
+ if ( 'likes' == $column_name ) {
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $blog_id = get_current_blog_id();
+ } else {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ }
+
+ $permalink = get_permalink( get_the_ID() ); ?>
+ <a title="" data-post-id="<?php echo (int) $post_id; ?>" class="post-com-count post-like-count" id="post-like-count-<?php echo (int) $post_id; ?>" data-blog-id="<?php echo (int) $blog_id; ?>" href="<?php echo esc_url( $permalink ); ?>#like-<?php echo (int) $post_id; ?>">
+ <span class="comment-count">0</span>
+ </a>
+ <?php
+ }
+ }
+
+ /**
+ * Add a "Likes" column header to the post edit table in wp-admin.
+ *
+ * @param array $columns
+ * @return array
+ */
+ function add_like_count_column( $columns ) {
+ $date = $columns['date'];
+ unset( $columns['date'] );
+
+ $columns['likes'] = '<span class="vers"><img title="' . esc_attr__( 'Likes', 'jetpack' ) . '" alt="' . esc_attr__( 'Likes', 'jetpack' ) . '" src="//s0.wordpress.com/i/like-grey-icon.png" /><span class="screen-reader-text">' . __( 'Likes', 'jetpack' ) . '</span></span>';
+ $columns['date'] = $date;
+
+ return $columns;
+ }
+
+ function post_likes( $content ) {
+ $post_id = get_the_ID();
+
+ if ( ! is_numeric( $post_id ) || ! $this->settings->is_likes_visible() )
+ return $content;
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $blog_id = get_current_blog_id();
+ $bloginfo = get_blog_details( (int) $blog_id );
+ $domain = $bloginfo->domain;
+ } else {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ $url = home_url();
+ $url_parts = wp_parse_url( $url );
+ $domain = $url_parts['host'];
+ }
+ // make sure to include the scripts before the iframe otherwise weird things happen
+ add_action( 'wp_footer', 'jetpack_likes_master_iframe', 21 );
+
+ /**
+ * if the same post appears more then once on a page the page goes crazy
+ * we need a slightly more unique id / name for the widget wrapper.
+ */
+ $uniqid = uniqid();
+
+ $src = sprintf( 'https://widgets.wp.com/likes/#blog_id=%1$d&amp;post_id=%2$d&amp;origin=%3$s&amp;obj_id=%1$d-%2$d-%4$s', $blog_id, $post_id, $domain, $uniqid );
+ $name = sprintf( 'like-post-frame-%1$d-%2$d-%3$s', $blog_id, $post_id, $uniqid );
+ $wrapper = sprintf( 'like-post-wrapper-%1$d-%2$d-%3$s', $blog_id, $post_id, $uniqid );
+ $headline = sprintf(
+ /** This filter is already documented in modules/sharedaddy/sharing-service.php */
+ apply_filters( 'jetpack_sharing_headline_html', '<h3 class="sd-title">%s</h3>', esc_html__( 'Like this:', 'jetpack' ), 'likes' ),
+ esc_html__( 'Like this:', 'jetpack' )
+ );
+
+ $html = "<div class='sharedaddy sd-block sd-like jetpack-likes-widget-wrapper jetpack-likes-widget-unloaded' id='$wrapper' data-src='$src' data-name='$name'>";
+ $html .= $headline;
+ $html .= "<div class='likes-widget-placeholder post-likes-widget-placeholder' style='height: 55px;'><span class='button'><span>" . esc_html__( 'Like', 'jetpack' ) . '</span></span> <span class="loading">' . esc_html__( 'Loading...', 'jetpack' ) . '</span></div>';
+ $html .= "<span class='sd-text-color'></span><a class='sd-link-color'></a>";
+ $html .= '</div>';
+
+ // Let's make sure that the script is enqueued
+ wp_enqueue_script( 'jetpack_likes_queuehandler' );
+
+ return $content . $html;
+ }
+
+ function post_flair_service_enabled_like( $classes ) {
+ $classes[] = 'sd-like-enabled';
+ return $classes;
+ }
+
+ function is_admin_bar_button_visible() {
+ global $wp_admin_bar;
+
+ if ( ! is_object( $wp_admin_bar ) )
+ return false;
+
+ if ( ( ! is_singular( 'post' ) && ! is_attachment() && ! is_page() ) )
+ return false;
+
+ if ( ! $this->settings->is_likes_visible() )
+ return false;
+
+ if ( ! $this->settings->is_post_likeable() )
+ return false;
+
+ /**
+ * Filters whether the Like button is enabled in the admin bar.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param bool true Should the Like button be visible in the Admin bar. Default to true.
+ */
+ return (bool) apply_filters( 'jetpack_admin_bar_likes_enabled', true );
+ }
+
+ function admin_bar_likes() {
+ global $wp_admin_bar;
+
+ $post_id = get_the_ID();
+
+ if ( ! is_numeric( $post_id ) || ! $this->is_admin_bar_button_visible() ) {
+ return;
+ }
+
+ $protocol = 'http';
+ if ( is_ssl() )
+ $protocol = 'https';
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $blog_id = get_current_blog_id();
+ $bloginfo = get_blog_details( (int) $blog_id );
+ $domain = $bloginfo->domain;
+ } else {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ $url = home_url();
+ $url_parts = wp_parse_url( $url );
+ $domain = $url_parts['host'];
+ }
+ // make sure to include the scripts before the iframe otherwise weird things happen
+ add_action( 'wp_footer', 'jetpack_likes_master_iframe', 21 );
+
+ $src = sprintf( 'https://widgets.wp.com/likes/#blog_id=%2$d&amp;post_id=%3$d&amp;origin=%1$s://%4$s', $protocol, $blog_id, $post_id, $domain );
+
+ $html = "<iframe class='admin-bar-likes-widget jetpack-likes-widget' scrolling='no' frameBorder='0' name='admin-bar-likes-widget' src='$src'></iframe>";
+
+ $node = array(
+ 'id' => 'admin-bar-likes-widget',
+ 'meta' => array(
+ 'html' => $html
+ )
+ );
+
+ $wp_admin_bar->add_node( $node );
+ }
+}
+
+/**
+ * Callback to get the value for the jetpack_likes_enabled field.
+ *
+ * Warning: this behavior is somewhat complicated!
+ * When the switch_like_status post_meta is unset, we follow the global setting in Sharing.
+ * When it is set to 0, we disable likes on the post, regardless of the global setting.
+ * When it is set to 1, we enable likes on the post, regardless of the global setting.
+ */
+function jetpack_post_likes_get_value( array $post ) {
+ $post_likes_switched = get_post_meta( $post['id'], 'switch_like_status', true );
+
+ /** This filter is documented in modules/jetpack-likes-settings.php */
+ $sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) );
+
+ // an empty string: post meta was not set, so go with the global setting
+ if ( "" === $post_likes_switched ) {
+ return $sitewide_likes_enabled;
+ }
+
+ // user overrode the global setting to disable likes
+ elseif ( "0" === $post_likes_switched ) {
+ return false;
+ }
+
+ // user overrode the global setting to enable likes
+ elseif ( "1" === $post_likes_switched ) {
+ return true;
+ }
+
+ // no default fallback, let's stay explicit
+}
+
+/**
+ * Callback to set switch_like_status post_meta when jetpack_likes_enabled is updated.
+ *
+ * Warning: this behavior is somewhat complicated!
+ * When the switch_like_status post_meta is unset, we follow the global setting in Sharing.
+ * When it is set to 0, we disable likes on the post, regardless of the global setting.
+ * When it is set to 1, we enable likes on the post, regardless of the global setting.
+ */
+function jetpack_post_likes_update_value( $enable_post_likes, $post_object ) {
+ /** This filter is documented in modules/jetpack-likes-settings.php */
+ $sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) );
+
+ $should_switch_status = $enable_post_likes !== $sitewide_likes_enabled;
+
+ if ( $should_switch_status ) {
+ // set the meta to 0 if the user wants to disable likes, 1 if user wants to enable
+ $switch_like_status = ( $enable_post_likes ? 1 : 0 );
+ return update_post_meta( $post_object->ID, 'switch_like_status', $switch_like_status );
+ } else {
+ // unset the meta otherwise
+ return delete_post_meta( $post_object->ID, 'switch_like_status' );
+ }
+}
+
+/**
+ * Add Likes post_meta to the REST API Post response.
+ *
+ * @action rest_api_init
+ * @uses register_rest_field
+ * @link https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-responses/
+ */
+function jetpack_post_likes_register_rest_field() {
+ $post_types = get_post_types( array( 'public' => true ) );
+ foreach ( $post_types as $post_type ) {
+ register_rest_field(
+ $post_type,
+ 'jetpack_likes_enabled',
+ array(
+ 'get_callback' => 'jetpack_post_likes_get_value',
+ 'update_callback' => 'jetpack_post_likes_update_value',
+ 'schema' => array(
+ 'description' => __( 'Are Likes enabled?', 'jetpack' ),
+ 'type' => 'boolean',
+ ),
+ )
+ );
+
+ /**
+ * Ensures all public internal post-types support `likes`
+ * This feature support flag is used by the REST API and Gutenberg.
+ */
+ add_post_type_support( $post_type, 'jetpack-post-likes' );
+ }
+}
+
+// Add Likes post_meta to the REST API Post response.
+add_action( 'rest_api_init', 'jetpack_post_likes_register_rest_field' );
+
+// Some CPTs (e.g. Jetpack portfolios and testimonials) get registered with
+// restapi_theme_init because they depend on theme support, so let's also hook to that
+add_action( 'restapi_theme_init', 'jetpack_post_likes_register_rest_field', 20 );
+
+/**
+ * Set the Likes and Sharing Gutenberg extension availability
+ */
+function jetpack_post_likes_set_extension_availability() {
+ Jetpack_Gutenberg::set_extension_available( 'likes' );
+}
+
+add_action( 'jetpack_register_gutenberg_extensions', 'jetpack_post_likes_set_extension_availability' );
+
+Jetpack_Likes::init();
diff --git a/plugins/jetpack/modules/likes/jetpack-likes-master-iframe.php b/plugins/jetpack/modules/likes/jetpack-likes-master-iframe.php
new file mode 100644
index 00000000..9ccca4c1
--- /dev/null
+++ b/plugins/jetpack/modules/likes/jetpack-likes-master-iframe.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * This function needs to get loaded after the like scripts get added to the page.
+ */
+function jetpack_likes_master_iframe() {
+ $version = gmdate( 'YW' );
+ $in_jetpack = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ? false : true;
+
+ $_locale = get_locale();
+
+ // We have to account for w.org vs WP.com locale divergence
+ if ( $in_jetpack ) {
+ if ( ! defined( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) || ! file_exists( JETPACK__GLOTPRESS_LOCALES_PATH ) ) {
+ return false;
+ }
+
+ require_once JETPACK__GLOTPRESS_LOCALES_PATH;
+
+ $gp_locale = GP_Locales::by_field( 'wp_locale', $_locale );
+ $_locale = isset( $gp_locale->slug ) ? $gp_locale->slug : '';
+ }
+
+ $likes_locale = ( '' == $_locale || 'en' == $_locale ) ? '' : '&amp;lang=' . strtolower( $_locale );
+
+ $src = sprintf(
+ 'https://widgets.wp.com/likes/master.html?ver=%1$s#ver=%1$s%2$s',
+ $version,
+ $likes_locale
+ );
+
+ /* translators: The value of %d is not available at the time of output */
+ $likersText = wp_kses( __( '<span>%d</span> bloggers like this:', 'jetpack' ), array( 'span' => array() ) );
+ ?>
+ <iframe src='<?php echo $src; ?>' scrolling='no' id='likes-master' name='likes-master' style='display:none;'></iframe>
+ <div id='likes-other-gravatars'><div class="likes-text"><?php echo $likersText; ?></div><ul class="wpl-avatars sd-like-gravatars"></ul></div>
+ <?php
+}
diff --git a/plugins/jetpack/modules/likes/jetpack-likes-settings.php b/plugins/jetpack/modules/likes/jetpack-likes-settings.php
new file mode 100644
index 00000000..390fea1e
--- /dev/null
+++ b/plugins/jetpack/modules/likes/jetpack-likes-settings.php
@@ -0,0 +1,729 @@
+<?php
+
+class Jetpack_Likes_Settings {
+ function __construct() {
+ $this->in_jetpack = ! ( defined( 'IS_WPCOM' ) && IS_WPCOM );
+ }
+
+ /**
+ * Replaces the "Sharing" title for the post screen metabox with "Likes and Shares"
+ */
+ public function add_likes_to_sharing_meta_box_title() {
+ return __( 'Likes and Shares', 'jetpack' );
+ }
+
+ /**
+ * Adds a metabox to the post screen if the sharing one doesn't currently exist.
+ */
+ public function add_meta_box() {
+ if (
+ /**
+ * Allow disabling of the Likes metabox on the post editor screen.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param bool false Should the Likes metabox be disabled? Default to false.
+ */
+ apply_filters( 'post_flair_disable', false )
+ ) {
+ return;
+ }
+
+ $post_types = get_post_types( array( 'public' => true ) );
+ /**
+ * Filters the Likes metabox title.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param string Likes metabox title. Default to "Likes".
+ */
+ $title = apply_filters( 'likes_meta_box_title', __( 'Likes', 'jetpack' ) );
+ foreach( $post_types as $post_type ) {
+ add_meta_box( 'likes_meta', $title, array( $this, 'meta_box_content' ), $post_type, 'side', 'default', array( '__back_compat_meta_box' => true ) );
+ }
+ }
+
+ /**
+ * Shows the likes option in the post screen metabox.
+ */
+ public function meta_box_content( $post ) {
+ $post_id = ! empty( $post->ID ) ? (int) $post->ID : get_the_ID();
+ $checked = true;
+ $disabled = ! $this->is_enabled_sitewide();
+ $switched_status = get_post_meta( $post_id, 'switch_like_status', true );
+
+ if ( $disabled && empty( $switched_status ) || ! $disabled && $switched_status === '0' ) {
+ $checked = false;
+ }
+
+ /**
+ * Fires before the Likes meta box content in the post editor.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param WP_Post|array|null $post Post data.
+ */
+ do_action( 'start_likes_meta_box_content', $post );
+ ?>
+
+ <p>
+ <label for="wpl_enable_post_likes">
+ <input type="checkbox" name="wpl_enable_post_likes" id="wpl_enable_post_likes" value="1" <?php checked( $checked ); ?>>
+ <?php esc_html_e( 'Show likes.', 'jetpack' ); ?>
+ </label>
+ <input type="hidden" name="wpl_like_status_hidden" value="1" />
+ </p> <?php
+ /**
+ * Fires after the Likes meta box content in the post editor.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param WP_Post|array|null $post Post data.
+ */
+ do_action( 'end_likes_meta_box_content', $post );
+ }
+
+ /**
+ * Returns the current state of the "WordPress.com Likes are" option.
+ * @return boolean true if enabled sitewide, false if not
+ */
+ public function is_enabled_sitewide() {
+ /**
+ * Filters whether Likes are enabled by default on all posts.
+ * true if enabled sitewide, false if not.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param bool $option Are Likes enabled sitewide.
+ */
+ return (bool) apply_filters( 'wpl_is_enabled_sitewide', ! Jetpack_Options::get_option_and_ensure_autoload( 'disabled_likes', 0 ) );
+ }
+
+ public function meta_box_save( $post_id ) {
+ if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) {
+ return $post_id;
+ }
+
+ if ( empty( $_POST['wpl_like_status_hidden'] ) ) {
+ return $post_id;
+ }
+
+ // Record sharing disable. Only needs to be done for WPCOM
+ if ( ! $this->in_jetpack ) {
+ if ( isset( $_POST['post_type'] ) && in_array( $_POST['post_type'], get_post_types( array( 'public' => true ) ) ) ) {
+ if ( ! isset( $_POST['wpl_enable_post_sharing'] ) ) {
+ update_post_meta( $post_id, 'sharing_disabled', 1 );
+ } else {
+ delete_post_meta( $post_id, 'sharing_disabled' );
+ }
+ }
+ }
+
+ if ( 'post' == $_POST['post_type'] ) {
+ if ( !current_user_can( 'edit_post', $post_id ) ) {
+ return $post_id;
+ }
+ }
+
+ // Record a change in like status for this post - only if it contradicts the
+ // site like setting. If it doesn't contradict, then we delete the new individual status.
+ if ( ! $this->is_enabled_sitewide() && ! empty( $_POST['wpl_enable_post_likes'] ) ) {
+ // Likes turned on for individual posts. User wants to add the button to a single post
+ update_post_meta( $post_id, 'switch_like_status', 1 );
+ } else if ( $this->is_enabled_sitewide() && empty( $_POST['wpl_enable_post_likes'] ) ) {
+ // Likes turned on for all posts. User wants to remove the button from a single post
+ update_post_meta( $post_id, 'switch_like_status', 0 );
+ } else if (
+ ( ! $this->is_enabled_sitewide() && empty( $_POST['wpl_enable_post_likes'] ) ) ||
+ ( $this->is_enabled_sitewide() && ! empty( $_POST['wpl_enable_post_likes'] ) )
+ ) {
+ // User wants to update the likes button status for an individual post, but the new status
+ // is the same as if they're asking for the default behavior according to the current Likes setting.
+ // So we delete the meta.
+ delete_post_meta( $post_id, 'switch_like_status' );
+ }
+
+ return $post_id;
+ }
+
+ /**
+ * WordPress.com: Metabox option for sharing (sharedaddy will handle this on the JP blog)
+ */
+ public function sharing_meta_box_content( $post ) {
+ $post_id = ! empty( $post->ID ) ? (int) $post->ID : get_the_ID();
+ $disabled = get_post_meta( $post_id, 'sharing_disabled', true ); ?>
+ <p>
+ <label for="wpl_enable_post_sharing">
+ <input type="checkbox" name="wpl_enable_post_sharing" id="wpl_enable_post_sharing" value="1" <?php checked( ! $disabled ); ?>>
+ <?php _e( 'Show sharing buttons.', 'jetpack' ); ?>
+ </label>
+ <input type="hidden" name="wpl_sharing_status_hidden" value="1" />
+ </p> <?php
+ }
+
+ /**
+ * Adds the 'sharing' menu to the settings menu.
+ * Only ran if sharedaddy and publicize are not already active.
+ */
+ function sharing_menu() {
+ add_submenu_page( 'options-general.php', esc_html__( 'Sharing Settings', 'jetpack' ), esc_html__( 'Sharing', 'jetpack' ), 'manage_options', 'sharing', array( $this, 'sharing_page' ) );
+ }
+
+ /**
+ * Provides a sharing page with the sharing_global_options hook
+ * so we can display the setting.
+ * Only ran if sharedaddy and publicize are not already active.
+ */
+ function sharing_page() {
+ $this->updated_message(); ?>
+ <div class="wrap">
+ <div class="icon32" id="icon-options-general"><br /></div>
+ <h1><?php esc_html_e( 'Sharing Settings', 'jetpack' ); ?></h1>
+ <?php
+ /** This action is documented in modules/sharedaddy/sharing.php */
+ do_action( 'pre_admin_screen_sharing' );
+ ?>
+ <?php $this->sharing_block(); ?>
+ </div> <?php
+ }
+
+ /**
+ * Returns the settings have been saved message.
+ */
+ function updated_message() {
+ if ( isset( $_GET['update'] ) && $_GET['update'] == 'saved' ){
+ echo '<div class="updated"><p>' . esc_html__( 'Settings have been saved', 'jetpack' ) . '</p></div>';
+ }
+ }
+
+ /**
+ * Returns just the "sharing buttons" w/ like option block, so it can be inserted into different sharing page contexts
+ */
+ function sharing_block() { ?>
+ <h2><?php esc_html_e( 'Sharing Buttons', 'jetpack' ); ?></h2>
+ <form method="post" action="">
+ <table class="form-table">
+ <tbody>
+ <?php
+ /** This action is documented in modules/sharedaddy/sharing.php */
+ do_action( 'sharing_global_options' );
+ ?>
+ </tbody>
+ </table>
+
+ <p class="submit">
+ <input type="submit" name="submit" class="button-primary" value="<?php esc_attr_e( 'Save Changes', 'jetpack' ); ?>" />
+ </p>
+
+ <input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce( 'sharing-options' );?>" />
+ </form> <?php
+ }
+
+ /**
+ * Are likes enabled for this post?
+ *
+ * @param int $post_id
+ * @return bool
+ */
+ function is_post_likeable( $post_id = 0 ) {
+ $post = get_post( $post_id );
+ if ( ! $post || is_wp_error( $post ) ) {
+ return false;
+ }
+
+ $sitewide_likes_enabled = (bool) $this->is_enabled_sitewide();
+ $post_likes_switched = get_post_meta( $post->ID, 'switch_like_status', true );
+
+ // on WPCOM, we need to look at post edit date so we don't break old posts
+ // if post edit date predates this code, stick with the former (buggy) behavior
+ // see: p7DVsv-64H-p2
+ $last_modified_time = strtotime( $post->post_modified_gmt );
+
+ $behavior_was_changed_at = strtotime( "2019-02-22 00:40:42" );
+
+ if ( $this->in_jetpack || $last_modified_time > $behavior_was_changed_at ) {
+ // the new and improved behavior on Jetpack and recent WPCOM posts:
+ // $post_likes_switched is empty to follow site setting,
+ // 0 if we want likes disabled, 1 if we want likes enabled
+ return $post_likes_switched || ( $sitewide_likes_enabled && $post_likes_switched !== '0' );
+ }
+
+ // implicit else (old behavior): $post_likes_switched simply inverts the global setting
+ return ( (bool) $post_likes_switched ) xor $sitewide_likes_enabled;
+ }
+
+ /**
+ * Are likes visible in this context?
+ *
+ * Some of this code was taken and modified from sharing_display() to ensure
+ * similar logic and filters apply here, too.
+ */
+ function is_likes_visible() {
+ require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
+ if ( Jetpack_Sync_Settings::is_syncing() ) {
+ return false;
+ }
+
+ global $wp_current_filter; // Used to apply 'sharing_show' filter
+
+ $post = get_post();
+
+ // Never show on feeds or previews
+ if ( is_feed() || is_preview() ) {
+ $enabled = false;
+
+ // Not a feed or preview, so what is it?
+ } else {
+
+ if ( in_the_loop() ) {
+ // If in the loop, check if the current post is likeable
+ $enabled = $this->is_post_likeable();
+ } else {
+ // Otherwise, check and see if likes are enabled sitewide
+ $enabled = $this->is_enabled_sitewide();
+ }
+
+ if ( post_password_required() )
+ $enabled = false;
+
+ if ( in_array( 'get_the_excerpt', (array) $wp_current_filter ) ) {
+ $enabled = false;
+ }
+
+ // Sharing Setting Overrides ****************************************
+
+ // Single post including custom post types
+ if ( is_single() ) {
+ if ( ! $this->is_single_post_enabled( $post->post_type ) ) {
+ $enabled = false;
+ }
+
+ // Single page
+ } elseif ( is_page() && ! is_front_page() ) {
+ if ( ! $this->is_single_page_enabled() ) {
+ $enabled = false;
+ }
+
+ // Attachment
+ } elseif ( is_attachment() ) {
+ if ( ! $this->is_attachment_enabled() ) {
+ $enabled = false;
+ }
+
+ // All other loops
+ } elseif ( ! $this->is_index_enabled() ) {
+ $enabled = false;
+ }
+ }
+
+ if ( $post instanceof WP_Post ) {
+ // Check that the post is a public, published post.
+ if ( 'attachment' == $post->post_type ) {
+ $post_status = get_post_status( $post->post_parent );
+ } else {
+ $post_status = $post->post_status;
+ }
+ if ( 'publish' != $post_status ) {
+ $enabled = false;
+ }
+ }
+
+ // Run through the sharing filters
+ /** This filter is documented in modules/sharedaddy/sharing-service.php */
+ $enabled = apply_filters( 'sharing_show', $enabled, $post );
+
+ /**
+ * Filters whether the Likes should be visible or not.
+ * Allows overwriting the options set in Settings > Sharing.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param bool $enabled Should the Likes be visible?
+ */
+ return (bool) apply_filters( 'wpl_is_likes_visible', $enabled );
+ }
+
+ /**
+ * Are Post Likes enabled on single posts?
+ *
+ * @param String $post_type custom post type identifier
+ * @return bool
+ */
+ function is_single_post_enabled( $post_type = 'post' ) {
+ $options = $this->get_options();
+ return (bool) apply_filters(
+ /**
+ * Filters whether Likes should be enabled on single posts.
+ *
+ * The dynamic part of the filter, {$post_type}, allows you to specific the post type where Likes should be enabled.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param bool $enabled Are Post Likes enabled on single posts?
+ */
+ "wpl_is_single_{$post_type}_disabled",
+ (bool) in_array( $post_type, $options['show'] )
+ );
+ }
+
+ /**
+ * Get the 'disabled_likes' option from the DB of the current blog.
+ *
+ * @return array
+ */
+ function get_options() {
+ $setting = array();
+ $setting['disabled'] = get_option( 'disabled_likes' );
+ $sharing = get_option( 'sharing-options' );
+
+ // Default visibility settings
+ if ( ! isset( $sharing['global']['show'] ) ) {
+ $sharing['global']['show'] = array( 'post', 'page' );
+
+ // Scalar check
+ } elseif ( is_scalar( $sharing['global']['show'] ) ) {
+ switch ( $sharing['global']['show'] ) {
+ case 'posts' :
+ $sharing['global']['show'] = array( 'post', 'page' );
+ break;
+ case 'index' :
+ $sharing['global']['show'] = array( 'index' );
+ break;
+ case 'posts-index' :
+ $sharing['global']['show'] = array( 'post', 'page', 'index' );
+ break;
+ }
+ }
+
+ // Ensure it's always an array (even if not previously empty or scalar)
+ $setting['show'] = ! empty( $sharing['global']['show'] ) ? (array) $sharing['global']['show'] : array();
+
+ /**
+ * Filters where the Likes are displayed.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param array $setting Array of Likes display settings.
+ */
+ return apply_filters( 'wpl_get_options', $setting );
+ }
+
+ /**
+ * Are Post Likes enabled on archive/front/search pages?
+ *
+ * @return bool
+ */
+ function is_index_enabled() {
+ $options = $this->get_options();
+ /**
+ * Filters whether Likes should be enabled on archive/front/search pages.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param bool $enabled Are Post Likes enabled on archive/front/search pages?
+ */
+ return (bool) apply_filters( 'wpl_is_index_disabled', (bool) in_array( 'index', $options['show'] ) );
+ }
+
+ /**
+ * Are Post Likes enabled on single pages?
+ *
+ * @return bool
+ */
+ function is_single_page_enabled() {
+ $options = $this->get_options();
+ /**
+ * Filters whether Likes should be enabled on single pages.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param bool $enabled Are Post Likes enabled on single pages?
+ */
+ return (bool) apply_filters( 'wpl_is_single_page_disabled', (bool) in_array( 'page', $options['show'] ) );
+ }
+
+ /**
+ * Are Media Likes enabled on single pages?
+ *
+ * @return bool
+ */
+ function is_attachment_enabled() {
+ $options = $this->get_options();
+ /**
+ * Filters whether Likes should be enabled on attachment pages.
+ *
+ * @module likes
+ *
+ * @since 2.2.0
+ *
+ * @param bool $enabled Are Post Likes enabled on attachment pages?
+ */
+ return (bool) apply_filters( 'wpl_is_attachment_disabled', (bool) in_array( 'attachment', $options['show'] ) );
+ }
+
+ /**
+ * The actual options block to be inserted into the sharing page.
+ */
+ function admin_settings_init() {
+ ?>
+ <tr>
+ <th scope="row">
+ <label><?php esc_html_e( 'WordPress.com Likes are', 'jetpack' ); ?></label>
+ </th>
+ <td>
+ <div>
+ <label>
+ <input type="radio" class="code" name="wpl_default" value="on" <?php checked( $this->is_enabled_sitewide(), true ); ?> />
+ <?php esc_html_e( 'On for all posts', 'jetpack' ); ?>
+ </label>
+ </div>
+ <div>
+ <label>
+ <input type="radio" class="code" name="wpl_default" value="off" <?php checked( $this->is_enabled_sitewide(), false ); ?> />
+ <?php esc_html_e( 'Turned on per post', 'jetpack' ); ?>
+ </label>
+ <div>
+ </td>
+ </tr>
+ <?php if ( ! $this->in_jetpack ) : ?>
+ <tr>
+ <th scope="row">
+ <label><?php esc_html_e( 'WordPress.com Reblog Button', 'jetpack' ); ?></label>
+ </th>
+ <td>
+ <div>
+ <label>
+ <input type="radio" class="code" name="jetpack_reblogs_enabled" value="on" <?php checked( $this->reblogs_enabled_sitewide(), true ); ?> />
+ <?php esc_html_e( 'Show the Reblog button on posts', 'jetpack' ); ?>
+ </label>
+ </div>
+ <div>
+ <label>
+ <input type="radio" class="code" name="jetpack_reblogs_enabled" value="off" <?php checked( $this->reblogs_enabled_sitewide(), false ); ?> />
+ <?php esc_html_e( 'Don\'t show the Reblog button on posts', 'jetpack' ); ?>
+ </label>
+ </div>
+ </td>
+ </tr>
+ <!-- WPCOM only: Comment Likes -->
+ <?php if ( ! $this->in_jetpack ) : ?>
+ <tr>
+ <th scope="row">
+ <label><?php esc_html_e( 'Comment Likes are', 'jetpack' ); ?></label>
+ </th>
+ <td>
+ <div>
+ <label>
+ <input type="checkbox" class="code" name="jetpack_comment_likes_enabled" value="1" <?php checked( $this->is_comments_enabled(), true ); ?> />
+ <?php esc_html_e( 'On for all comments', 'jetpack' ); ?>
+ </label>
+ </div>
+ </td>
+ </tr>
+ <?php endif; ?>
+ <?php endif; ?>
+ </tbody> <?php // closes the tbody attached to sharing_show_buttons_on_row_start... ?>
+ <?php
+ }
+
+ /**
+ * Returns the current state of the "WordPress.com Reblogs are" option.
+ * @return boolean true if enabled sitewide, false if not
+ */
+ function reblogs_enabled_sitewide() {
+ /**
+ * Filters whether Reblogs are enabled by default on all posts.
+ * true if enabled sitewide, false if not.
+ *
+ * @module likes
+ *
+ * @since 3.0.0
+ *
+ * @param bool $option Are Reblogs enabled sitewide.
+ */
+ return (bool) apply_filters( 'wpl_reblogging_enabled_sitewide', ! get_option( 'disabled_reblogs' ) );
+ }
+
+ /**
+ * Used for WPCOM ONLY. Comment likes are in their own module in Jetpack.
+ * Returns if comment likes are enabled. Defaults to 'off'
+ * @return boolean true if we should show comment likes, false if not
+ */
+ function is_comments_enabled() {
+ /**
+ * Filters whether Comment Likes are enabled.
+ * true if enabled, false if not.
+ *
+ * @module comment-likes
+ *
+ * @since 2.2.0
+ *
+ * @param bool $option Are Comment Likes enabled sitewide.
+ */
+ return (bool) apply_filters( 'jetpack_comment_likes_enabled', get_option( 'jetpack_comment_likes_enabled', false ) );
+ }
+
+ /**
+ * Saves the setting in the database, bumps a stat on WordPress.com
+ */
+ function admin_settings_callback() {
+ // We're looking for these, and doing a dance to set some stats and save
+ // them together in array option.
+ $new_state = ! empty( $_POST['wpl_default'] ) ? $_POST['wpl_default'] : 'on';
+ $db_state = $this->is_enabled_sitewide();
+
+ $reblogs_new_state = ! empty( $_POST['jetpack_reblogs_enabled'] ) ? $_POST['jetpack_reblogs_enabled'] : 'on';
+ $reblogs_db_state = $this->reblogs_enabled_sitewide();
+ /** Default State *********************************************************/
+
+ // Checked (enabled)
+ switch( $new_state ) {
+ case 'off' :
+ if ( true == $db_state && ! $this->in_jetpack ) {
+ $g_gif = file_get_contents( 'https://pixel.wp.com/g.gif?v=wpcom-no-pv&x_likes=disabled_likes' );
+ }
+ update_option( 'disabled_likes', 1 );
+ break;
+ case 'on' :
+ default:
+ if ( false == $db_state && ! $this->in_jetpack ) {
+ $g_gif = file_get_contents( 'https://pixel.wp.com/g.gif?v=wpcom-no-pv&x_likes=reenabled_likes' );
+ }
+ delete_option( 'disabled_likes' );
+ break;
+ }
+
+ switch( $reblogs_new_state ) {
+ case 'off' :
+ if ( true == $reblogs_db_state && ! $this->in_jetpack ) {
+ $g_gif = file_get_contents( 'https://pixel.wp.com/g.gif?v=wpcom-no-pv&x_reblogs=disabled_reblogs' );
+ }
+ update_option( 'disabled_reblogs', 1 );
+ break;
+ case 'on' :
+ default:
+ if ( false == $reblogs_db_state && ! $this->in_jetpack ) {
+ $g_gif = file_get_contents( 'https://pixel.wp.com/g.gif?v=wpcom-no-pv&x_reblogs=reenabled_reblogs' );
+ }
+ delete_option( 'disabled_reblogs' );
+ break;
+ }
+
+ // WPCOM only: Comment Likes
+ if ( ! $this->in_jetpack ) {
+ $new_comments_state = ! empty( $_POST['jetpack_comment_likes_enabled'] ) ? $_POST['jetpack_comment_likes_enabled'] : false;
+ switch( (bool) $new_comments_state ) {
+ case true:
+ update_option( 'jetpack_comment_likes_enabled', 1 );
+ break;
+ case false:
+ default:
+ update_option( 'jetpack_comment_likes_enabled', 0 );
+ break;
+ }
+ }
+ }
+
+ /**
+ * Adds the admin update hook so we can save settings even if Sharedaddy is not enabled.
+ */
+ function process_update_requests_if_sharedaddy_not_loaded() {
+ if ( isset( $_GET['page'] ) && ( $_GET['page'] == 'sharing.php' || $_GET['page'] == 'sharing' ) ) {
+ if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'sharing-options' ) ) {
+ /** This action is documented in modules/sharedaddy/sharing.php */
+ do_action( 'sharing_admin_update' );
+ wp_safe_redirect( admin_url( 'options-general.php?page=sharing&update=saved' ) );
+ die();
+ }
+ }
+ }
+
+ /**
+ * If sharedaddy is not loaded, we don't have the "Show buttons on" yet, so we need to add that since it affects likes too.
+ */
+ function admin_settings_showbuttonon_init() {
+ /** This action is documented in modules/sharedaddy/sharing.php */
+ echo apply_filters( 'sharing_show_buttons_on_row_start', '<tr valign="top">' );
+ ?>
+ <th scope="row"><label><?php _e( 'Show buttons on', 'jetpack' ); ?></label></th>
+ <td>
+ <?php
+ $br = false;
+ $shows = array_values( get_post_types( array( 'public' => true ) ) );
+ array_unshift( $shows, 'index' );
+ $global = $this->get_options();
+ foreach ( $shows as $show ) :
+ if ( 'index' == $show ) {
+ $label = __( 'Front Page, Archive Pages, and Search Results', 'jetpack' );
+ } else {
+ $post_type_object = get_post_type_object( $show );
+ $label = $post_type_object->labels->name;
+ }
+ ?>
+ <?php if ( $br ) echo '<br />'; ?><label><input type="checkbox"<?php checked( in_array( $show, $global['show'] ) ); ?> name="show[]" value="<?php echo esc_attr( $show ); ?>" /> <?php echo esc_html( $label ); ?></label>
+ <?php $br = true; endforeach; ?>
+ </td>
+ <?php
+ /** This action is documented in modules/sharedaddy/sharing.php */
+ echo apply_filters( 'sharing_show_buttons_on_row_end', '</tr>' );
+ ?>
+ <?php
+ }
+
+ /**
+ * If sharedaddy is not loaded, we still need to save the the settings of the "Show buttons on" option.
+ */
+ function admin_settings_showbuttonon_callback() {
+ $options = get_option( 'sharing-options' );
+ if ( !is_array( $options ) )
+ $options = array();
+
+ $shows = array_values( get_post_types( array( 'public' => true ) ) );
+ $shows[] = 'index';
+ $data = $_POST;
+
+ if ( isset( $data['show'] ) ) {
+ if ( is_scalar( $data['show'] ) ) {
+ switch ( $data['show'] ) {
+ case 'posts' :
+ $data['show'] = array( 'post', 'page' );
+ break;
+ case 'index' :
+ $data['show'] = array( 'index' );
+ break;
+ case 'posts-index' :
+ $data['show'] = array( 'post', 'page', 'index' );
+ break;
+ }
+ }
+
+ if ( $data['show'] = array_intersect( $data['show'], $shows ) ) {
+ $options['global']['show'] = $data['show'];
+ }
+ } else {
+ $options['global']['show'] = array();
+ }
+
+ update_option( 'sharing-options', $options );
+ }
+}
diff --git a/plugins/jetpack/modules/likes/post-count-jetpack.js b/plugins/jetpack/modules/likes/post-count-jetpack.js
new file mode 100644
index 00000000..1059e9bd
--- /dev/null
+++ b/plugins/jetpack/modules/likes/post-count-jetpack.js
@@ -0,0 +1,20 @@
+var wpPostLikeCount = wpPostLikeCount || {};
+
+( function( $ ) {
+ wpPostLikeCount = jQuery.extend( wpPostLikeCount, {
+ request: function( options ) {
+ return $.ajax( {
+ type: 'GET',
+ url: wpPostLikeCount.jsonAPIbase + options.path,
+ dataType: 'jsonp',
+ data: options.data,
+ success: function( response ) {
+ options.success( response );
+ },
+ error: function( response ) {
+ options.error( response );
+ },
+ } );
+ },
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/likes/post-count.js b/plugins/jetpack/modules/likes/post-count.js
new file mode 100644
index 00000000..1ddfe561
--- /dev/null
+++ b/plugins/jetpack/modules/likes/post-count.js
@@ -0,0 +1,64 @@
+/* jshint onevar: false, smarttabs: true */
+
+var wpPostLikeCount = wpPostLikeCount || {};
+
+( function( $ ) {
+ wpPostLikeCount = jQuery.extend( wpPostLikeCount, {
+ jsonAPIbase: 'https://public-api.wordpress.com/rest/v1',
+ APIqueue: [],
+
+ wpPostLikeCount: function() {
+ $( '.post-like-count' ).each( function() {
+ var post_id = $( this ).attr( 'data-post-id' );
+ var blog_id = $( this ).attr( 'data-blog-id' );
+ wpPostLikeCount.APIqueue.push( '/sites/' + blog_id + '/posts/' + post_id + '/likes' );
+ } );
+ wpPostLikeCount.getCounts();
+ },
+
+ showCount: function( post_id, count ) {
+ if ( count > 0 ) {
+ $( '#post-like-count-' + post_id )
+ .find( '.comment-count' )
+ .hide();
+ $( '#post-like-count-' + post_id )
+ .find( '.comment-count' )
+ .text( count );
+ $( '#post-like-count-' + post_id )
+ .find( '.comment-count' )
+ .fadeIn();
+ }
+ },
+
+ getCounts: function() {
+ var batchRequest = {
+ path: '/batch',
+ data: '',
+ success: function( response ) {
+ for ( var path in response ) {
+ if ( ! response[ path ].error_data ) {
+ var urlPieces = path.split( '/' ); // pieces[4] = post id;
+ var post_id = urlPieces[ 4 ];
+ wpPostLikeCount.showCount( post_id, response[ path ].found );
+ }
+ }
+ },
+ error: function(/*response*/) {},
+ };
+
+ var amp = '';
+ for ( var i = 0; i < wpPostLikeCount.APIqueue.length; i++ ) {
+ if ( i > 0 ) {
+ amp = '&';
+ }
+ batchRequest.data += amp + 'urls[]=' + wpPostLikeCount.APIqueue[ i ];
+ }
+
+ wpPostLikeCount.request( batchRequest );
+ },
+ } );
+} )( jQuery );
+
+jQuery( document ).ready( function(/*$*/) {
+ wpPostLikeCount.wpPostLikeCount();
+} );
diff --git a/plugins/jetpack/modules/likes/queuehandler.js b/plugins/jetpack/modules/likes/queuehandler.js
new file mode 100644
index 00000000..df6970f2
--- /dev/null
+++ b/plugins/jetpack/modules/likes/queuehandler.js
@@ -0,0 +1,402 @@
+/* global pm, wpcom_reblog, JSON */
+
+var jetpackLikesWidgetBatch = [];
+var jetpackLikesMasterReady = false;
+
+// Due to performance problems on pages with a large number of widget iframes that need to be loaded,
+// we are limiting the processing at any instant to unloaded widgets that are currently in viewport,
+// plus this constant that will allow processing of widgets above and bellow the current fold.
+// This aim of it is to improve the UX and hide the transition from unloaded to loaded state from users.
+var jetpackLikesLookAhead = 2000; // pixels
+
+// Keeps track of loaded comment likes widget so we can unload them when they are scrolled out of view.
+var jetpackCommentLikesLoadedWidgets = [];
+
+function JetpackLikesPostMessage( message, target ) {
+ if ( 'string' === typeof message ) {
+ try {
+ message = JSON.parse( message );
+ } catch ( e ) {
+ return;
+ }
+ }
+
+ pm( {
+ target: target,
+ type: 'likesMessage',
+ data: message,
+ origin: '*',
+ } );
+}
+
+function JetpackLikesBatchHandler() {
+ var requests = [];
+ jQuery( 'div.jetpack-likes-widget-unloaded' ).each( function() {
+ if ( jetpackLikesWidgetBatch.indexOf( this.id ) > -1 ) {
+ return;
+ }
+
+ if ( ! jetpackIsScrolledIntoView( this ) ) {
+ return;
+ }
+
+ jetpackLikesWidgetBatch.push( this.id );
+
+ var regex = /like-(post|comment)-wrapper-(\d+)-(\d+)-(\w+)/,
+ match = regex.exec( this.id ),
+ info;
+
+ if ( ! match || match.length !== 5 ) {
+ return;
+ }
+
+ info = {
+ blog_id: match[ 2 ],
+ width: this.width,
+ };
+
+ if ( 'post' === match[ 1 ] ) {
+ info.post_id = match[ 3 ];
+ } else if ( 'comment' === match[ 1 ] ) {
+ info.comment_id = match[ 3 ];
+ }
+
+ info.obj_id = match[ 4 ];
+
+ requests.push( info );
+ } );
+
+ if ( requests.length > 0 ) {
+ JetpackLikesPostMessage(
+ { event: 'initialBatch', requests: requests },
+ window.frames[ 'likes-master' ]
+ );
+ }
+}
+
+function JetpackLikesMessageListener( event, message ) {
+ var allowedOrigin, $container, $list, offset, rowLength, height, scrollbarWidth;
+
+ if ( 'undefined' === typeof event.event ) {
+ return;
+ }
+
+ // We only allow messages from one origin
+ allowedOrigin = 'https://widgets.wp.com';
+ if ( allowedOrigin !== message.origin ) {
+ return;
+ }
+
+ switch ( event.event ) {
+ case 'masterReady':
+ jQuery( document ).ready( function() {
+ jetpackLikesMasterReady = true;
+
+ var stylesData = {
+ event: 'injectStyles',
+ },
+ $sdTextColor = jQuery( '.sd-text-color' ),
+ $sdLinkColor = jQuery( '.sd-link-color' );
+
+ if ( jQuery( 'iframe.admin-bar-likes-widget' ).length > 0 ) {
+ JetpackLikesPostMessage( { event: 'adminBarEnabled' }, window.frames[ 'likes-master' ] );
+
+ stylesData.adminBarStyles = {
+ background: jQuery( '#wpadminbar .quicklinks li#wp-admin-bar-wpl-like > a' ).css(
+ 'background'
+ ),
+ isRtl: 'rtl' === jQuery( '#wpadminbar' ).css( 'direction' ),
+ };
+ }
+
+ if ( ! window.addEventListener ) {
+ jQuery( '#wp-admin-bar-admin-bar-likes-widget' ).hide();
+ }
+
+ stylesData.textStyles = {
+ color: $sdTextColor.css( 'color' ),
+ fontFamily: $sdTextColor.css( 'font-family' ),
+ fontSize: $sdTextColor.css( 'font-size' ),
+ direction: $sdTextColor.css( 'direction' ),
+ fontWeight: $sdTextColor.css( 'font-weight' ),
+ fontStyle: $sdTextColor.css( 'font-style' ),
+ textDecoration: $sdTextColor.css( 'text-decoration' ),
+ };
+
+ stylesData.linkStyles = {
+ color: $sdLinkColor.css( 'color' ),
+ fontFamily: $sdLinkColor.css( 'font-family' ),
+ fontSize: $sdLinkColor.css( 'font-size' ),
+ textDecoration: $sdLinkColor.css( 'text-decoration' ),
+ fontWeight: $sdLinkColor.css( 'font-weight' ),
+ fontStyle: $sdLinkColor.css( 'font-style' ),
+ };
+
+ JetpackLikesPostMessage( stylesData, window.frames[ 'likes-master' ] );
+
+ JetpackLikesBatchHandler();
+ } );
+
+ break;
+
+ case 'showLikeWidget':
+ jQuery( '#' + event.id + ' .likes-widget-placeholder' ).fadeOut( 'fast' );
+ break;
+
+ case 'showCommentLikeWidget':
+ jQuery( '#' + event.id + ' .likes-widget-placeholder' ).fadeOut( 'fast' );
+ break;
+
+ case 'killCommentLikes':
+ // If kill switch for comment likes is enabled remove all widgets wrappers and `Loading...` placeholders.
+ jQuery( '.jetpack-comment-likes-widget-wrapper' ).remove();
+ break;
+
+ case 'clickReblogFlair':
+ wpcom_reblog.toggle_reblog_box_flair( event.obj_id );
+ break;
+
+ case 'showOtherGravatars':
+ $container = jQuery( '#likes-other-gravatars' );
+ $list = $container.find( 'ul' );
+
+ $container.hide();
+ $list.html( '' );
+
+ $container.find( '.likes-text span' ).text( event.total );
+
+ jQuery.each( event.likers, function( i, liker ) {
+ var element;
+
+ if ( 'http' !== liker.profile_URL.substr( 0, 4 ) ) {
+ // We only display gravatars with http or https schema
+ return;
+ }
+
+ element = jQuery( '<li><a><img /></a></li>' );
+ element.addClass( liker.css_class );
+
+ element
+ .find( 'a' )
+ .attr( {
+ href: liker.profile_URL,
+ rel: 'nofollow',
+ target: '_parent',
+ } )
+ .addClass( 'wpl-liker' );
+
+ element
+ .find( 'img' )
+ .attr( {
+ src: liker.avatar_URL,
+ alt: liker.name,
+ } )
+ .css( {
+ width: '30px',
+ height: '30px',
+ paddingRight: '3px',
+ } );
+
+ $list.append( element );
+ } );
+
+ offset = jQuery( 'body' )
+ .find( "[name='" + event.parent + "']" )
+ .offset();
+
+ $container.css( 'left', offset.left + event.position.left - 10 + 'px' );
+ $container.css( 'top', offset.top + event.position.top - 33 + 'px' );
+
+ rowLength = Math.floor( event.width / 37 );
+ height = Math.ceil( event.likers.length / rowLength ) * 37 + 13;
+ if ( height > 204 ) {
+ height = 204;
+ }
+
+ $container.css( 'height', height + 'px' );
+ $container.css( 'width', rowLength * 37 - 7 + 'px' );
+
+ $list.css( 'width', rowLength * 37 + 'px' );
+
+ $container.fadeIn( 'slow' );
+
+ scrollbarWidth = $list[ 0 ].offsetWidth - $list[ 0 ].clientWidth;
+ if ( scrollbarWidth > 0 ) {
+ $container.width( $container.width() + scrollbarWidth );
+ $list.width( $list.width() + scrollbarWidth );
+ }
+ }
+}
+
+pm.bind( 'likesMessage', JetpackLikesMessageListener );
+
+jQuery( document ).click( function( e ) {
+ var $container = jQuery( '#likes-other-gravatars' );
+
+ if ( $container.has( e.target ).length === 0 ) {
+ $container.fadeOut( 'slow' );
+ }
+} );
+
+function JetpackLikesWidgetQueueHandler() {
+ var wrapperID;
+
+ if ( ! jetpackLikesMasterReady ) {
+ setTimeout( JetpackLikesWidgetQueueHandler, 500 );
+ return;
+ }
+
+ // Restore widgets to initial unloaded state when they are scrolled out of view.
+ jetpackUnloadScrolledOutWidgets();
+
+ var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView();
+
+ if ( unloadedWidgetsInView.length > 0 ) {
+ // Grab any unloaded widgets for a batch request
+ JetpackLikesBatchHandler();
+ }
+
+ for ( var i = 0, length = unloadedWidgetsInView.length; i <= length - 1; i++ ) {
+ wrapperID = unloadedWidgetsInView[ i ].id;
+
+ if ( ! wrapperID ) {
+ continue;
+ }
+
+ jetpackLoadLikeWidgetIframe( wrapperID );
+ }
+}
+
+function jetpackLoadLikeWidgetIframe( wrapperID ) {
+ var $wrapper;
+
+ if ( 'undefined' === typeof wrapperID ) {
+ return;
+ }
+
+ $wrapper = jQuery( '#' + wrapperID );
+ $wrapper.find( 'iframe' ).remove();
+
+ var placeholder = $wrapper.find( '.likes-widget-placeholder' );
+
+ // Post like iframe
+ if ( placeholder.hasClass( 'post-likes-widget-placeholder' ) ) {
+ var postLikesFrame = document.createElement( 'iframe' );
+
+ postLikesFrame[ 'class' ] = 'post-likes-widget jetpack-likes-widget';
+ postLikesFrame.name = $wrapper.data( 'name' );
+ postLikesFrame.src = $wrapper.data( 'src' );
+ postLikesFrame.height = '18px';
+ postLikesFrame.width = '200px';
+ postLikesFrame.frameBorder = '0';
+ postLikesFrame.scrolling = 'no';
+
+ if ( $wrapper.hasClass( 'slim-likes-widget' ) ) {
+ postLikesFrame.height = '22px';
+ postLikesFrame.width = '68px';
+ postLikesFrame.scrolling = 'no';
+ } else {
+ postLikesFrame.height = '55px';
+ postLikesFrame.width = '100%';
+ }
+
+ placeholder.after( postLikesFrame );
+ }
+
+ // Comment like iframe
+ if ( placeholder.hasClass( 'comment-likes-widget-placeholder' ) ) {
+ var commentLikesFrame = document.createElement( 'iframe' );
+
+ commentLikesFrame[ 'class' ] = 'comment-likes-widget-frame jetpack-likes-widget-frame';
+ commentLikesFrame.name = $wrapper.data( 'name' );
+ commentLikesFrame.src = $wrapper.data( 'src' );
+ commentLikesFrame.height = '18px';
+ commentLikesFrame.width = '100%';
+ commentLikesFrame.frameBorder = '0';
+ commentLikesFrame.scrolling = 'no';
+
+ $wrapper.find( '.comment-like-feedback' ).after( commentLikesFrame );
+
+ jetpackCommentLikesLoadedWidgets.push( commentLikesFrame );
+ }
+
+ $wrapper
+ .removeClass( 'jetpack-likes-widget-unloaded' )
+ .addClass( 'jetpack-likes-widget-loading' );
+
+ $wrapper.find( 'iframe' ).load( function( e ) {
+ var $iframe = jQuery( e.target );
+
+ JetpackLikesPostMessage(
+ { event: 'loadLikeWidget', name: $iframe.attr( 'name' ), width: $iframe.width() },
+ window.frames[ 'likes-master' ]
+ );
+
+ $wrapper
+ .removeClass( 'jetpack-likes-widget-loading' )
+ .addClass( 'jetpack-likes-widget-loaded' );
+
+ if ( $wrapper.hasClass( 'slim-likes-widget' ) ) {
+ $wrapper.find( 'iframe' ).Jetpack( 'resizeable' );
+ }
+ } );
+}
+
+function jetpackGetUnloadedWidgetsInView() {
+ var $unloadedWidgets = jQuery( 'div.jetpack-likes-widget-unloaded' );
+
+ return $unloadedWidgets.filter( function() {
+ return jetpackIsScrolledIntoView( this );
+ } );
+}
+
+function jetpackIsScrolledIntoView( element ) {
+ var top = element.getBoundingClientRect().top;
+ var bottom = element.getBoundingClientRect().bottom;
+
+ // Allow some slack above and bellow the fold with jetpackLikesLookAhead,
+ // with the aim of hiding the transition from unloaded to loaded widget from users.
+ return top + jetpackLikesLookAhead >= 0 && bottom <= window.innerHeight + jetpackLikesLookAhead;
+}
+
+function jetpackUnloadScrolledOutWidgets() {
+ for ( var i = jetpackCommentLikesLoadedWidgets.length - 1; i >= 0; i-- ) {
+ var currentWidgetIframe = jetpackCommentLikesLoadedWidgets[ i ];
+
+ if ( ! jetpackIsScrolledIntoView( currentWidgetIframe ) ) {
+ var $widgetWrapper = jQuery( currentWidgetIframe )
+ .parent()
+ .parent();
+
+ // Restore parent class to 'unloaded' so this widget can be picked up by queue manager again if needed.
+ $widgetWrapper
+ .removeClass( 'jetpack-likes-widget-loaded jetpack-likes-widget-loading' )
+ .addClass( 'jetpack-likes-widget-unloaded' );
+
+ // Bring back the loading placeholder into view.
+ $widgetWrapper.children( '.comment-likes-widget-placeholder' ).fadeIn();
+
+ // Remove it from the list of loaded widgets.
+ jetpackCommentLikesLoadedWidgets.splice( i, 1 );
+
+ // Remove comment like widget iFrame.
+ jQuery( currentWidgetIframe ).remove();
+ }
+ }
+}
+
+var jetpackWidgetsDelayedExec = function( after, fn ) {
+ var timer;
+ return function() {
+ timer && clearTimeout( timer );
+ timer = setTimeout( fn, after );
+ };
+};
+
+var jetpackOnScrollStopped = jetpackWidgetsDelayedExec( 250, JetpackLikesWidgetQueueHandler );
+
+// Load initial batch of widgets, prior to any scrolling events.
+JetpackLikesWidgetQueueHandler();
+
+// Add event listener to execute queue handler after scroll.
+window.addEventListener( 'scroll', jetpackOnScrollStopped, true );
diff --git a/plugins/jetpack/modules/likes/style.css b/plugins/jetpack/modules/likes/style.css
new file mode 100644
index 00000000..e76b0c2d
--- /dev/null
+++ b/plugins/jetpack/modules/likes/style.css
@@ -0,0 +1,233 @@
+/**
+ * Like Button toolbar button, loading text & container styles
+ */
+
+@font-face {
+ font-family: Noticons;
+ src: url(https://wordpress.com/i/noticons/Noticons.woff);
+}
+
+/* Master container */
+#jp-post-flair {
+ padding-top: .5em;
+}
+
+/* Overall Sharedaddy block title */
+div.sharedaddy,
+#content div.sharedaddy,
+#main div.sharedaddy {
+ clear: both;
+}
+
+div.sharedaddy h3.sd-title {
+ margin: 0 0 1em 0;
+ display: inline-block;
+ line-height: 1.2;
+ font-size: 9pt;
+ font-weight: bold;
+}
+
+div.sharedaddy h3.sd-title:before {
+ content: "";
+ display: block;
+ width: 100%;
+ min-width: 30px;
+ border-top: 1px solid #ddd;
+ margin-bottom: 1em;
+}
+
+
+/* Toolbar */
+#wpadminbar li#wp-admin-bar-admin-bar-likes-widget {
+ width: 61px;
+ overflow: hidden;
+}
+
+#wpadminbar iframe.admin-bar-likes-widget {
+ width: 61px;
+ height: 28px;
+ min-height: 28px;
+ border-width: 0px;
+ position: absolute;
+ top: 0;
+}
+
+div.jetpack-likes-widget-wrapper {
+ width: 100%;
+ min-height: 50px; /* Previous height, 60px */
+ position: relative; /* Need to abs position placeholder and iframe so there isn't a jarring jump */
+}
+
+div.jetpack-likes-widget-wrapper .sd-link-color {
+ font-size: 12px;
+}
+
+div.jetpack-likes-widget-wrapper.slim-likes-widget {
+ width: 1px; /* initial default */
+ min-height: 0;
+}
+
+div.jetpack-comment-likes-widget-wrapper {
+ width: 100%;
+ position: relative;
+ min-height: 31px;
+}
+
+div.jetpack-comment-likes-widget-wrapper iframe {
+ margin-bottom: 0;
+}
+
+#likes-other-gravatars {
+ display: none;
+ position: absolute;
+ padding: 10px 10px 12px 10px;
+ background-color: #2e4453;
+ border-width: 0;
+ box-shadow: 0 0 10px #2e4453;
+ box-shadow: 0 0 10px rgba(46,68,83,.6);
+ min-width: 130px;
+ z-index: 1000;
+}
+
+#likes-other-gravatars * {
+ line-height: normal;
+}
+
+#likes-other-gravatars .likes-text {
+ color: white;
+ font-size: 12px;
+ padding-bottom: 8px;
+}
+
+#likes-other-gravatars ul,
+#likes-other-gravatars li {
+ margin: 0;
+ padding: 0;
+ text-indent: 0;
+ list-style-type: none;
+}
+
+#likes-other-gravatars li::before {
+ content: "";
+}
+
+#likes-other-gravatars ul.wpl-avatars {
+ overflow: auto;
+ display: block;
+ max-height: 190px;
+}
+
+#likes-other-gravatars ul.wpl-avatars li {
+ width: 32px;
+ height: 32px;
+ float: left;
+ margin: 0 5px 5px 0;
+}
+
+#likes-other-gravatars ul.wpl-avatars li a {
+ margin: 0 2px 0 0;
+ border-bottom: none !important;
+ display: block;
+}
+
+#likes-other-gravatars ul.wpl-avatars li a img {
+ background: none;
+ border: none;
+ margin: 0 !important;
+ padding: 0 !important;
+ position: static;
+}
+
+div.sd-box {
+ border-top: 1px solid #ddd;
+ border-top: 1px solid rgba(0,0,0,.13);
+}
+
+.entry-content .post-likes-widget, .post-likes-widget,
+.comment-likes-widget {
+ margin: 0;
+ border-width: 0;
+ display: block;
+}
+
+/* Loading text */
+.post-likes-widget-placeholder,
+.comment-likes-widget-placeholder {
+ margin: 0;
+ border-width: 0;
+ position: relative;
+}
+
+.comment-likes-widget-placeholder {
+ height: 18px;
+ position: absolute;
+ display: flex;
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
+ margin-top: 4px;
+}
+
+.comment-likes-widget-placeholder::before {
+ -webkit-font-smoothing: antialiased;
+ font-family: "Noticons";
+ font-size: 20px;
+ line-height: .9;
+ color: #5CB5D4;
+ content: '\f408';
+ width: 16px;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.post-likes-widget-placeholder .button {
+ display: none; /* Let's not show a dummy like button, let's just make a great button experience once it's loaded */
+}
+
+.post-likes-widget-placeholder .button span {
+}
+
+.post-likes-widget-placeholder .loading,
+.comment-likes-widget-placeholder .loading {
+ color: #999;
+ font-size: 12px;
+}
+
+.comment-likes-widget-placeholder .loading {
+ padding-left: 5px;
+ margin-top: 2px;
+ align-self: center;
+ color: #4E4E4E;
+}
+
+.slim-likes-widget .post-likes-widget {
+ width: auto;
+ float: none;
+}
+
+/* Like Special cases (display on it's own) */
+div.sharedaddy.sd-like-enabled .sd-like h3 {
+ display: none;
+}
+
+div.sharedaddy.sd-like-enabled .sd-like .post-likes-widget {
+ width: 100%;
+ float: none;
+ position: absolute; /* Need to abs position placeholder and iframe so there isn't a jarring jump */
+ top: 0;
+}
+
+.comment-likes-widget {
+ width: 100%;
+}
+
+
+/* Make ratings block. @todo: make !important unnecessary by removing inline style */
+.pd-rating,
+.cs-rating {
+ display: block !important;
+}
+
+
+/* Hide G+ title */
+.sd-gplus .sd-title {
+ display: none;
+}
diff --git a/plugins/jetpack/modules/manage.php b/plugins/jetpack/modules/manage.php
new file mode 100644
index 00000000..b7f5b9a7
--- /dev/null
+++ b/plugins/jetpack/modules/manage.php
@@ -0,0 +1,4 @@
+<?php
+/**
+ * Manage is no longer a module. It is baked directly into Jetpack, as its functionality is required for Jetpack to do what it does.
+ */
diff --git a/plugins/jetpack/modules/manage/activate-admin.php b/plugins/jetpack/modules/manage/activate-admin.php
new file mode 100644
index 00000000..4dad036a
--- /dev/null
+++ b/plugins/jetpack/modules/manage/activate-admin.php
@@ -0,0 +1,4 @@
+<?php
+/*
+ * Stub, so that updates don't fatal due to "missing files".
+ */
diff --git a/plugins/jetpack/modules/manage/confirm-admin.php b/plugins/jetpack/modules/manage/confirm-admin.php
new file mode 100644
index 00000000..4dad036a
--- /dev/null
+++ b/plugins/jetpack/modules/manage/confirm-admin.php
@@ -0,0 +1,4 @@
+<?php
+/*
+ * Stub, so that updates don't fatal due to "missing files".
+ */
diff --git a/plugins/jetpack/modules/markdown.php b/plugins/jetpack/modules/markdown.php
new file mode 100644
index 00000000..719095a6
--- /dev/null
+++ b/plugins/jetpack/modules/markdown.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * Module Name: Markdown
+ * Module Description: Write posts or pages in plain-text Markdown syntax
+ * Sort Order: 31
+ * First Introduced: 2.8
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Writing
+ * Feature: Writing
+ * Additional Search Queries: md, markdown
+ */
+
+include dirname( __FILE__ ) . '/markdown/easy-markdown.php';
+
+/**
+ * Remove checkbox set in modules/markdown/easy-markdown.php.
+ * We don't just remove the register_setting call there because the checkbox is
+ * needed on WordPress.com, where the file is sync'ed verbatim.
+ */
+function jetpack_markdown_posting_always_on() {
+ // why oh why isn't there a remove_settings_field?
+ global $wp_settings_fields;
+ if ( isset( $wp_settings_fields['writing']['default'][ WPCom_Markdown::POST_OPTION ] ) ) {
+ unset( $wp_settings_fields['writing']['default'][ WPCom_Markdown::POST_OPTION ] );
+ }
+}
+add_action( 'admin_init', 'jetpack_markdown_posting_always_on', 11 );
diff --git a/plugins/jetpack/modules/markdown/easy-markdown.php b/plugins/jetpack/modules/markdown/easy-markdown.php
new file mode 100644
index 00000000..06a2cab9
--- /dev/null
+++ b/plugins/jetpack/modules/markdown/easy-markdown.php
@@ -0,0 +1,812 @@
+<?php
+
+/*
+Plugin Name: Easy Markdown
+Plugin URI: http://automattic.com/
+Description: Write in Markdown, publish in WordPress
+Version: 0.1
+Author: Matt Wiebe
+Author URI: http://automattic.com/
+*/
+
+/**
+ * Copyright (c) Automattic. All rights reserved.
+ *
+ * Released under the GPL license
+ * http://www.opensource.org/licenses/gpl-license.php
+ *
+ * This is an add-on for WordPress
+ * https://wordpress.org/
+ *
+ * **********************************************************************
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * **********************************************************************
+ */
+
+class WPCom_Markdown {
+
+
+ const POST_OPTION = 'wpcom_publish_posts_with_markdown';
+ const COMMENT_OPTION = 'wpcom_publish_comments_with_markdown';
+ const POST_TYPE_SUPPORT = 'wpcom-markdown';
+ const IS_MD_META = '_wpcom_is_markdown';
+
+ private static $parser;
+ private static $instance;
+
+ // to ensure that our munged posts over xml-rpc are removed from the cache
+ public $posts_to_uncache = array();
+ private $monitoring = array( 'post' => array(), 'parent' => array() );
+
+
+ /**
+ * Yay singletons!
+ * @return object WPCom_Markdown instance
+ */
+ public static function get_instance() {
+ if ( ! self::$instance )
+ self::$instance = new self();
+ return self::$instance;
+ }
+
+ /**
+ * Kicks things off on `init` action
+ * @return null
+ */
+ public function load() {
+ $this->add_default_post_type_support();
+ $this->maybe_load_actions_and_filters();
+ if ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) {
+ add_action( 'switch_blog', array( $this, 'maybe_load_actions_and_filters' ), 10, 2 );
+ }
+ add_action( 'admin_init', array( $this, 'register_setting' ) );
+ add_action( 'admin_init', array( $this, 'maybe_unload_for_bulk_edit' ) );
+ if ( current_theme_supports( 'o2' ) || class_exists( 'P2' ) ) {
+ $this->add_o2_helpers();
+ }
+ }
+
+ /**
+ * If we're in a bulk edit session, unload so that we don't lose our markdown metadata
+ * @return null
+ */
+ public function maybe_unload_for_bulk_edit() {
+ if ( isset( $_REQUEST['bulk_edit'] ) && $this->is_posting_enabled() ) {
+ $this->unload_markdown_for_posts();
+ }
+ }
+
+ /**
+ * Called on init and fires on switch_blog to decide if our actions and filters
+ * should be running.
+ * @param int|null $new_blog_id New blog ID
+ * @param int|null $old_blog_id Old blog ID
+ * @return null
+ */
+ public function maybe_load_actions_and_filters( $new_blog_id = null, $old_blog_id = null ) {
+ // If this is a switch_to_blog call, and the blog isn't changing, we'll already be loaded
+ if ( $new_blog_id && $new_blog_id === $old_blog_id ) {
+ return;
+ }
+
+ if ( $this->is_posting_enabled() ) {
+ $this->load_markdown_for_posts();
+ } else {
+ $this->unload_markdown_for_posts();
+ }
+
+ if ( $this->is_commenting_enabled() ) {
+ $this->load_markdown_for_comments();
+ } else {
+ $this->unload_markdown_for_comments();
+ }
+ }
+
+ /**
+ * Set up hooks for enabling Markdown conversion on posts
+ * @return null
+ */
+ public function load_markdown_for_posts() {
+ add_filter( 'wp_kses_allowed_html', array( $this, 'wp_kses_allowed_html' ), 10, 2 );
+ add_action( 'after_wp_tiny_mce', array( $this, 'after_wp_tiny_mce' ) );
+ add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) );
+ add_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 );
+ add_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 );
+ add_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 );
+ add_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 );
+ add_filter( '_wp_post_revision_fields', array( $this, '_wp_post_revision_fields' ) );
+ add_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) );
+ add_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 );
+ if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
+ $this->check_for_early_methods();
+ }
+ }
+
+ /**
+ * Removes hooks to disable Markdown conversion on posts
+ * @return null
+ */
+ public function unload_markdown_for_posts() {
+ remove_filter( 'wp_kses_allowed_html', array( $this, 'wp_kses_allowed_html' ) );
+ remove_action( 'after_wp_tiny_mce', array( $this, 'after_wp_tiny_mce' ) );
+ remove_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) );
+ remove_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 );
+ remove_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 );
+ remove_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 );
+ remove_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 );
+ remove_filter( '_wp_post_revision_fields', array( $this, '_wp_post_revision_fields' ) );
+ remove_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) );
+ remove_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 );
+ }
+
+ /**
+ * Set up hooks for enabling Markdown conversion on comments
+ * @return null
+ */
+ protected function load_markdown_for_comments() {
+ // Use priority 9 so that Markdown runs before KSES, which can clean up
+ // any munged HTML.
+ add_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 );
+ }
+
+ /**
+ * Removes hooks to disable Markdown conversion
+ * @return null
+ */
+ protected function unload_markdown_for_comments() {
+ remove_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 );
+ }
+
+ /**
+ * o2 does some of what we do. Let's take precedence.
+ * @return null
+ */
+ public function add_o2_helpers() {
+ if ( $this->is_posting_enabled() ) {
+ add_filter( 'content_save_pre', array( $this, 'o2_escape_lists' ), 1 );
+ }
+
+ add_filter( 'o2_preview_post', array( $this, 'o2_preview_post' ) );
+ add_filter( 'o2_preview_comment', array( $this, 'o2_preview_comment' ) );
+
+ add_filter( 'wpcom_markdown_transform_pre', array( $this, 'o2_unescape_lists' ) );
+ add_filter( 'wpcom_untransformed_content', array( $this, 'o2_unescape_lists' ) );
+ }
+
+ /**
+ * If Markdown is enabled for posts on this blog, filter the text for o2 previews
+ * @param string $text Post text
+ * @return string Post text transformed through the magic of Markdown
+ */
+ public function o2_preview_post( $text ) {
+ if ( $this->is_posting_enabled() ) {
+ $text = $this->transform( $text, array( 'unslash' => false ) );
+ }
+ return $text;
+ }
+
+ /**
+ * If Markdown is enabled for comments on this blog, filter the text for o2 previews
+ * @param string $text Comment text
+ * @return string Comment text transformed through the magic of Markdown
+ */
+ public function o2_preview_comment( $text ) {
+ if ( $this->is_commenting_enabled() ) {
+ $text = $this->transform( $text, array( 'unslash' => false ) );
+ }
+ return $text;
+ }
+
+ /**
+ * Escapes lists so that o2 doesn't trounce them
+ * @param string $text Post/comment text
+ * @return string Text escaped with HTML entity for asterisk
+ */
+ public function o2_escape_lists( $text ) {
+ return preg_replace( '/^\\* /um', '&#42; ', $text );
+ }
+
+ /**
+ * Unescapes the token we inserted on o2_escape_lists
+ * @param string $text Post/comment text with HTML entities for asterisks
+ * @return string Text with the HTML entity removed
+ */
+ public function o2_unescape_lists( $text ) {
+ return preg_replace( '/^[&]\#042; /um', '* ', $text );
+ }
+
+ /**
+ * Preserve code blocks from being munged by KSES before they have a chance
+ * @param string $text post content
+ * @return string post content with code blocks escaped
+ */
+ public function preserve_code_blocks( $text ) {
+ return $this->get_parser()->codeblock_preserve( $text );
+ }
+
+ /**
+ * Remove KSES if it's there. Store the result to manually invoke later if needed.
+ * @return null
+ */
+ public function maybe_remove_kses() {
+ // Filters return true if they existed before you removed them
+ if ( $this->is_posting_enabled() )
+ $this->kses = remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' ) && remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
+ }
+
+ /**
+ * Add our Writing and Discussion settings.
+ * @return null
+ */
+ public function register_setting() {
+ add_settings_field( self::POST_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'post_field' ), 'writing' );
+ register_setting( 'writing', self::POST_OPTION, array( $this, 'sanitize_setting') );
+ add_settings_field( self::COMMENT_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'comment_field' ), 'discussion' );
+ register_setting( 'discussion', self::COMMENT_OPTION, array( $this, 'sanitize_setting') );
+ }
+
+ /**
+ * Sanitize setting. Don't really want to store "on" value, so we'll store "1" instead!
+ * @param string $input Value received by settings API via $_POST
+ * @return bool Cast to boolean.
+ */
+ public function sanitize_setting( $input ) {
+ return (bool) $input;
+ }
+
+ /**
+ * Prints HTML for the Writing setting
+ * @return null
+ */
+ public function post_field() {
+ printf(
+ '<label><input name="%s" id="%s" type="checkbox"%s /> %s</label><p class="description">%s</p>',
+ self::POST_OPTION,
+ self::POST_OPTION,
+ checked( $this->is_posting_enabled(), true, false ),
+ esc_html__( 'Use Markdown for posts and pages.', 'jetpack' ),
+ sprintf( '<a href="%s">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) )
+ );
+ }
+
+ /**
+ * Prints HTML for the Discussion setting
+ * @return null
+ */
+ public function comment_field() {
+ printf(
+ '<label><input name="%s" id="%s" type="checkbox"%s /> %s</label><p class="description">%s</p>',
+ self::COMMENT_OPTION,
+ self::COMMENT_OPTION,
+ checked( $this->is_commenting_enabled(), true, false ),
+ esc_html__( 'Use Markdown for comments.', 'jetpack' ),
+ sprintf( '<a href="%s">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) )
+ );
+ }
+
+ /**
+ * Get the support url for Markdown
+ * @uses apply_filters
+ * @return string support url
+ */
+ protected function get_support_url() {
+ /**
+ * Filter the Markdown support URL.
+ *
+ * @module markdown
+ *
+ * @since 2.8.0
+ *
+ * @param string $url Markdown support URL.
+ */
+ return apply_filters( 'easy_markdown_support_url', 'http://en.support.wordpress.com/markdown-quick-reference/' );
+ }
+
+ /**
+ * Is Mardown conversion for posts enabled?
+ * @return boolean
+ */
+ public function is_posting_enabled() {
+ return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::POST_OPTION, '' );
+ }
+
+ /**
+ * Is Markdown conversion for comments enabled?
+ * @return boolean
+ */
+ public function is_commenting_enabled() {
+ return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::COMMENT_OPTION, '' );
+ }
+
+ /**
+ * Check if a $post_id has Markdown enabled
+ * @param int $post_id A post ID.
+ * @return boolean
+ */
+ public function is_markdown( $post_id ) {
+ return get_metadata( 'post', $post_id, self::IS_MD_META, true );
+ }
+
+ /**
+ * Set Markdown as enabled on a post_id. We skip over update_postmeta so we
+ * can sneakily set metadata on post revisions, which we need.
+ * @param int $post_id A post ID.
+ * @return bool The metadata was successfully set.
+ */
+ protected function set_as_markdown( $post_id ) {
+ return update_metadata( 'post', $post_id, self::IS_MD_META, true );
+ }
+
+ /**
+ * Get our Markdown parser object, optionally requiring all of our needed classes and
+ * instantiating our parser.
+ * @return object WPCom_GHF_Markdown_Parser instance.
+ */
+ public function get_parser() {
+
+ if ( ! self::$parser ) {
+ jetpack_require_lib( 'markdown' );
+ self::$parser = new WPCom_GHF_Markdown_Parser;
+ }
+
+ return self::$parser;
+ }
+
+ /**
+ * We don't want Markdown conversion all over the place.
+ * @return null
+ */
+ public function add_default_post_type_support() {
+ add_post_type_support( 'post', self::POST_TYPE_SUPPORT );
+ add_post_type_support( 'page', self::POST_TYPE_SUPPORT );
+ add_post_type_support( 'revision', self::POST_TYPE_SUPPORT );
+ }
+
+ /**
+ * Figure out the post type of the post screen we're on
+ * @return string Current post_type
+ */
+ protected function get_post_screen_post_type() {
+ global $pagenow;
+ if ( 'post-new.php' === $pagenow )
+ return ( isset( $_GET['post_type'] ) ) ? $_GET['post_type'] : 'post';
+ if ( isset( $_GET['post'] ) ) {
+ $post = get_post( (int) $_GET['post'] );
+ if ( is_object( $post ) && isset( $post->post_type ) )
+ return $post->post_type;
+ }
+ return 'post';
+ }
+
+ /**
+ * Swap post_content and post_content_filtered for editing
+ * @param string $content Post content
+ * @param int $id post ID
+ * @return string Swapped content
+ */
+ public function edit_post_content( $content, $id ) {
+ if ( $this->is_markdown( $id ) ) {
+ $post = get_post( $id );
+ if ( $post && ! empty( $post->post_content_filtered ) ) {
+ $post = $this->swap_for_editing( $post );
+ return $post->post_content;
+ }
+ }
+ return $content;
+ }
+
+ /**
+ * Swap post_content_filtered and post_content for editing
+ * @param string $content Post content_filtered
+ * @param int $id post ID
+ * @return string Swapped content
+ */
+ public function edit_post_content_filtered( $content, $id ) {
+ // if markdown was disabled, let's turn this off
+ if ( ! $this->is_posting_enabled() && $this->is_markdown( $id ) ) {
+ $post = get_post( $id );
+ if ( $post && ! empty( $post->post_content_filtered ) )
+ $content = '';
+ }
+ return $content;
+ }
+
+ /**
+ * Some tags are allowed to have a 'markdown' attribute, allowing them to contain Markdown.
+ * We need to tell KSES about those tags.
+ * @param array $tags List of tags that KSES allows.
+ * @param string $context The context that KSES is allowing these tags.
+ * @return array The tags that KSES allows, with our extra 'markdown' parameter where necessary.
+ */
+ public function wp_kses_allowed_html( $tags, $context ) {
+ if ( 'post' !== $context ) {
+ return $tags;
+ }
+
+ $re = '/' . $this->get_parser()->contain_span_tags_re . '/';
+ foreach ( $tags as $tag => $attributes ) {
+ if ( preg_match( $re, $tag ) ) {
+ $attributes['markdown'] = true;
+ $tags[ $tag ] = $attributes;
+ }
+ }
+
+ return $tags;
+ }
+
+ /**
+ * TinyMCE needs to know not to strip the 'markdown' attribute. Unfortunately, it doesn't
+ * really offer a nice API for whitelisting attributes, so we have to manually add it
+ * to the schema instead.
+ */
+ public function after_wp_tiny_mce() {
+?>
+<script type="text/javascript">
+jQuery( function() {
+ ( 'undefined' !== typeof tinymce ) && tinymce.on( 'AddEditor', function( event ) {
+ event.editor.on( 'BeforeSetContent', function( event ) {
+ var editor = event.target;
+ Object.keys( editor.schema.elements ).forEach( function( key, index ) {
+ editor.schema.elements[ key ].attributes['markdown'] = {};
+ editor.schema.elements[ key ].attributesOrder.push( 'markdown' );
+ } );
+ } );
+ }, true );
+} );
+</script>
+<?php
+ }
+
+ /**
+ * Magic happens here. Markdown is converted and stored on post_content. Original Markdown is stored
+ * in post_content_filtered so that we can continue editing as Markdown.
+ * @param array $post_data The post data that will be inserted into the DB. Slashed.
+ * @param array $postarr All the stuff that was in $_POST.
+ * @return array $post_data with post_content and post_content_filtered modified
+ */
+ public function wp_insert_post_data( $post_data, $postarr ) {
+ // $post_data array is slashed!
+ $post_id = isset( $postarr['ID'] ) ? $postarr['ID'] : false;
+ // bail early if markdown is disabled or this post type is unsupported.
+ if ( ! $this->is_posting_enabled() || ! post_type_supports( $post_data['post_type'], self::POST_TYPE_SUPPORT ) ) {
+ // it's disabled, but maybe this *was* a markdown post before.
+ if ( $this->is_markdown( $post_id ) && ! empty( $post_data['post_content_filtered'] ) ) {
+ $post_data['post_content_filtered'] = '';
+ }
+ // we have no context to determine supported post types in the `post_content_pre` hook,
+ // which already ran to sanitize code blocks. Undo that.
+ $post_data['post_content'] = $this->get_parser()->codeblock_restore( $post_data['post_content'] );
+ return $post_data;
+ }
+ // rejigger post_content and post_content_filtered
+ // revisions are already in the right place, except when we're restoring, but that's taken care of elsewhere
+ // also prevent quick edit feature from overriding already-saved markdown (issue https://github.com/Automattic/jetpack/issues/636)
+ if ( 'revision' !== $post_data['post_type'] && ! isset( $_POST['_inline_edit'] ) ) {
+ /**
+ * Filter the original post content passed to Markdown.
+ *
+ * @module markdown
+ *
+ * @since 2.8.0
+ *
+ * @param string $post_data['post_content'] Untransformed post content.
+ */
+ $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
+ $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_id ) );
+ /** This filter is already documented in core/wp-includes/default-filters.php */
+ $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
+ } elseif ( 0 === strpos( $post_data['post_name'], $post_data['post_parent'] . '-autosave' ) ) {
+ // autosaves for previews are weird
+ /** This filter is already documented in modules/markdown/easy-markdown.php */
+ $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
+ $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_data['post_parent'] ) );
+ /** This filter is already documented in core/wp-includes/default-filters.php */
+ $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
+ }
+
+ // set as markdown on the wp_insert_post hook later
+ if ( $post_id )
+ $this->monitoring['post'][ $post_id ] = true;
+ else
+ $this->monitoring['content'] = wp_unslash( $post_data['post_content'] );
+ if ( 'revision' === $postarr['post_type'] && $this->is_markdown( $postarr['post_parent'] ) )
+ $this->monitoring['parent'][ $postarr['post_parent'] ] = true;
+
+ return $post_data;
+ }
+
+ /**
+ * Calls on wp_insert_post action, after wp_insert_post_data. This way we can
+ * still set postmeta on our revisions after it's all been deleted.
+ * @param int $post_id The post ID that has just been added/updated
+ * @return null
+ */
+ public function wp_insert_post( $post_id ) {
+ $post_parent = get_post_field( 'post_parent', $post_id );
+ // this didn't have an ID yet. Compare the content that was just saved.
+ if ( isset( $this->monitoring['content'] ) && $this->monitoring['content'] === get_post_field( 'post_content', $post_id ) ) {
+ unset( $this->monitoring['content'] );
+ $this->set_as_markdown( $post_id );
+ }
+ if ( isset( $this->monitoring['post'][$post_id] ) ) {
+ unset( $this->monitoring['post'][$post_id] );
+ $this->set_as_markdown( $post_id );
+ } elseif ( isset( $this->monitoring['parent'][$post_parent] ) ) {
+ unset( $this->monitoring['parent'][$post_parent] );
+ $this->set_as_markdown( $post_id );
+ }
+ }
+
+ /**
+ * Run a comment through Markdown. Easy peasy.
+ * @param string $content
+ * @return string
+ */
+ public function pre_comment_content( $content ) {
+ return $this->transform( $content, array(
+ 'id' => $this->comment_hash( $content ),
+ ) );
+ }
+
+ protected function comment_hash( $content ) {
+ return 'c-' . substr( md5( $content ), 0, 8 );
+ }
+
+ /**
+ * Markdown conversion. Some DRYness for repetitive tasks.
+ * @param string $text Content to be run through Markdown
+ * @param array $args Arguments, with keys:
+ * id: provide a string to prefix footnotes with a unique identifier
+ * unslash: when true, expects and returns slashed data
+ * decode_code_blocks: when true, assume that text in fenced code blocks is already
+ * HTML encoded and should be decoded before being passed to Markdown, which does
+ * its own encoding.
+ * @return string Markdown-processed content
+ */
+ public function transform( $text, $args = array() ) {
+ // If this contains Gutenberg content, let's keep it intact.
+ if ( has_blocks( $text ) ) {
+ return $text;
+ }
+
+ $args = wp_parse_args( $args, array(
+ 'id' => false,
+ 'unslash' => true,
+ 'decode_code_blocks' => ! $this->get_parser()->use_code_shortcode
+ ) );
+ // probably need to unslash
+ if ( $args['unslash'] )
+ $text = wp_unslash( $text );
+
+ /**
+ * Filter the content to be run through Markdown, before it's transformed by Markdown.
+ *
+ * @module markdown
+ *
+ * @since 2.8.0
+ *
+ * @param string $text Content to be run through Markdown
+ * @param array $args Array of Markdown options.
+ */
+ $text = apply_filters( 'wpcom_markdown_transform_pre', $text, $args );
+ // ensure our paragraphs are separated
+ $text = str_replace( array( '</p><p>', "</p>\n<p>" ), "</p>\n\n<p>", $text );
+ // visual editor likes to add <p>s. Buh-bye.
+ $text = $this->get_parser()->unp( $text );
+ // sometimes we get an encoded > at start of line, breaking blockquotes
+ $text = preg_replace( '/^&gt;/m', '>', $text );
+ // prefixes are because we need to namespace footnotes by post_id
+ $this->get_parser()->fn_id_prefix = $args['id'] ? $args['id'] . '-' : '';
+ // If we're not using the code shortcode, prevent over-encoding.
+ if ( $args['decode_code_blocks'] ) {
+ $text = $this->get_parser()->codeblock_restore( $text );
+ }
+ // Transform it!
+ $text = $this->get_parser()->transform( $text );
+ // Fix footnotes - kses doesn't like the : IDs it supplies
+ $text = preg_replace( '/((id|href)="#?fn(ref)?):/', "$1-", $text );
+ // Markdown inserts extra spaces to make itself work. Buh-bye.
+ $text = rtrim( $text );
+ /**
+ * Filter the content to be run through Markdown, after it was transformed by Markdown.
+ *
+ * @module markdown
+ *
+ * @since 2.8.0
+ *
+ * @param string $text Content to be run through Markdown
+ * @param array $args Array of Markdown options.
+ */
+ $text = apply_filters( 'wpcom_markdown_transform_post', $text, $args );
+
+ // probably need to re-slash
+ if ( $args['unslash'] )
+ $text = wp_slash( $text );
+
+ return $text;
+ }
+
+ /**
+ * Shows Markdown in the Revisions screen, and ensures that post_content_filtered
+ * is maintained on revisions
+ * @param array $fields Post fields pertinent to revisions
+ * @return array Modified array to include post_content_filtered
+ */
+ public function _wp_post_revision_fields( $fields ) {
+ $fields['post_content_filtered'] = __( 'Markdown content', 'jetpack' );
+ return $fields;
+ }
+
+ /**
+ * Do some song and dance to keep all post_content and post_content_filtered content
+ * in the expected place when a post revision is restored.
+ * @param int $post_id The post ID have a restore done to it
+ * @param int $revision_id The revision ID being restored
+ * @return null
+ */
+ public function wp_restore_post_revision( $post_id, $revision_id ) {
+ if ( $this->is_markdown( $revision_id ) ) {
+ $revision = get_post( $revision_id, ARRAY_A );
+ $post = get_post( $post_id, ARRAY_A );
+ $post['post_content'] = $revision['post_content_filtered']; // Yes, we put it in post_content, because our wp_insert_post_data() expects that
+ // set this flag so we can restore the post_content_filtered on the last revision later
+ $this->monitoring['restore'] = true;
+ // let's not make a revision of our fixing update
+ add_filter( 'wp_revisions_to_keep', '__return_false', 99 );
+ wp_update_post( $post );
+ $this->fix_latest_revision_on_restore( $post_id );
+ remove_filter( 'wp_revisions_to_keep', '__return_false', 99 );
+ }
+ }
+
+ /**
+ * We need to ensure the last revision has Markdown, not HTML in its post_content_filtered
+ * column after a restore.
+ * @param int $post_id The post ID that was just restored.
+ * @return null
+ */
+ protected function fix_latest_revision_on_restore( $post_id ) {
+ global $wpdb;
+ $post = get_post( $post_id );
+ $last_revision = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_type = 'revision' AND post_parent = %d ORDER BY ID DESC", $post->ID ) );
+ $last_revision->post_content_filtered = $post->post_content_filtered;
+ wp_insert_post( (array) $last_revision );
+ }
+
+ /**
+ * Kicks off magic for an XML-RPC session. We want to keep editing Markdown
+ * and publishing HTML.
+ * @param string $xmlrpc_method The current XML-RPC method
+ * @return null
+ */
+ public function xmlrpc_actions( $xmlrpc_method ) {
+ switch ( $xmlrpc_method ) {
+ case 'metaWeblog.getRecentPosts':
+ case 'wp.getPosts':
+ case 'wp.getPages':
+ add_action( 'parse_query', array( $this, 'make_filterable' ), 10, 1 );
+ break;
+ case 'wp.getPost':
+ $this->prime_post_cache();
+ break;
+ }
+ }
+
+ /**
+ * metaWeblog.getPost and wp.getPage fire xmlrpc_call action *after* get_post() is called.
+ * So, we have to detect those methods and prime the post cache early.
+ * @return null
+ */
+ protected function check_for_early_methods() {
+ $raw_post_data = file_get_contents( "php://input" );
+ if ( false === strpos( $raw_post_data, 'metaWeblog.getPost' )
+ && false === strpos( $raw_post_data, 'wp.getPage' ) ) {
+ return;
+ }
+ include_once( ABSPATH . WPINC . '/class-IXR.php' );
+ $message = new IXR_Message( $raw_post_data );
+ $message->parse();
+ $post_id_position = 'metaWeblog.getPost' === $message->methodName ? 0 : 1;
+ $this->prime_post_cache( $message->params[ $post_id_position ] );
+ }
+
+ /**
+ * Prime the post cache with swapped post_content. This is a sneaky way of getting around
+ * the fact that there are no good hooks to call on the *.getPost xmlrpc methods.
+ *
+ * @return null
+ */
+ private function prime_post_cache( $post_id = false ) {
+ global $wp_xmlrpc_server;
+ if ( ! $post_id ) {
+ $post_id = $wp_xmlrpc_server->message->params[3];
+ }
+
+ // prime the post cache
+ if ( $this->is_markdown( $post_id ) ) {
+ $post = get_post( $post_id );
+ if ( ! empty( $post->post_content_filtered ) ) {
+ wp_cache_delete( $post->ID, 'posts' );
+ $post = $this->swap_for_editing( $post );
+ wp_cache_add( $post->ID, $post, 'posts' );
+ $this->posts_to_uncache[] = $post_id;
+ }
+ }
+ // uncache munged posts if using a persistent object cache
+ if ( wp_using_ext_object_cache() ) {
+ add_action( 'shutdown', array( $this, 'uncache_munged_posts' ) );
+ }
+ }
+
+ /**
+ * Swaps `post_content_filtered` back to `post_content` for editing purposes.
+ * @param object $post WP_Post object
+ * @return object WP_Post object with swapped `post_content_filtered` and `post_content`
+ */
+ protected function swap_for_editing( $post ) {
+ $markdown = $post->post_content_filtered;
+ // unencode encoded code blocks
+ $markdown = $this->get_parser()->codeblock_restore( $markdown );
+ // restore beginning of line blockquotes
+ $markdown = preg_replace( '/^&gt; /m', '> ', $markdown );
+ $post->post_content_filtered = $post->post_content;
+ $post->post_content = $markdown;
+ return $post;
+ }
+
+
+ /**
+ * We munge the post cache to serve proper markdown content to XML-RPC clients.
+ * Uncache these after the XML-RPC session ends.
+ * @return null
+ */
+ public function uncache_munged_posts() {
+ // $this context gets lost in testing sometimes. Weird.
+ foreach( WPCom_Markdown::get_instance()->posts_to_uncache as $post_id ) {
+ wp_cache_delete( $post_id, 'posts' );
+ }
+ }
+
+ /**
+ * Since *.(get)?[Rr]ecentPosts calls get_posts with suppress filters on, we need to
+ * turn them back on so that we can swap things for editing.
+ * @param object $wp_query WP_Query object
+ * @return null
+ */
+ public function make_filterable( $wp_query ) {
+ $wp_query->set( 'suppress_filters', false );
+ add_action( 'the_posts', array( $this, 'the_posts' ), 10, 2 );
+ }
+
+ /**
+ * Swaps post_content and post_content_filtered for editing.
+ * @param array $posts Posts returned by the just-completed query
+ * @param object $wp_query Current WP_Query object
+ * @return array Modified $posts
+ */
+ public function the_posts( $posts, $wp_query ) {
+ foreach ( $posts as $key => $post ) {
+ if ( $this->is_markdown( $post->ID ) && ! empty( $posts[ $key ]->post_content_filtered ) ) {
+ $markdown = $posts[ $key ]->post_content_filtered;
+ $posts[ $key ]->post_content_filtered = $posts[ $key ]->post_content;
+ $posts[ $key ]->post_content = $markdown;
+ }
+ }
+ return $posts;
+ }
+
+ /**
+ * Singleton silence is golden
+ */
+ private function __construct() {}
+}
+
+add_action( 'init', array( WPCom_Markdown::get_instance(), 'load' ) );
diff --git a/plugins/jetpack/modules/masterbar.php b/plugins/jetpack/modules/masterbar.php
new file mode 100644
index 00000000..8efeae3e
--- /dev/null
+++ b/plugins/jetpack/modules/masterbar.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Module Name: WordPress.com Toolbar
+ * Module Description: Replaces the admin bar with a useful toolbar to quickly manage your site via WordPress.com.
+ * Sort Order: 38
+ * Recommendation Order: 16
+ * First Introduced: 4.8
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: General
+ * Additional Search Queries: adminbar, masterbar
+ */
+
+require dirname( __FILE__ ) . '/masterbar/masterbar.php';
+
+// In order to be able to tell if it's an AMP request or not we have to hook into parse_query at a later priority.
+add_action( 'admin_bar_init', 'jetpack_initialize_masterbar', 99 );
+
+/**
+ * Initializes the Masterbar in case the request is not AMP.
+ */
+function jetpack_initialize_masterbar() {
+ if ( ! Jetpack_AMP_Support::is_amp_request() ) {
+ new A8C_WPCOM_Masterbar();
+ }
+}
diff --git a/plugins/jetpack/modules/masterbar/masterbar.php b/plugins/jetpack/modules/masterbar/masterbar.php
new file mode 100644
index 00000000..6a378b89
--- /dev/null
+++ b/plugins/jetpack/modules/masterbar/masterbar.php
@@ -0,0 +1,470 @@
+<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+
+require_once dirname( __FILE__ ) . '/rtl-admin-bar.php';
+
+/**
+ * Custom Admin bar displayed instead of the default WordPress admin bar.
+ */
+class A8C_WPCOM_Masterbar {
+ /**
+ * Use for testing changes made to remotely enqueued scripts and styles on your sandbox.
+ * If not set it will default to loading the ones from WordPress.com.
+ *
+ * @var string $sandbox_url
+ */
+ private $sandbox_url = '';
+
+ /**
+ * Current locale.
+ *
+ * @var string
+ */
+ private $locale;
+
+ /**
+ * Current User ID.
+ *
+ * @var int
+ */
+ private $user_id;
+ /**
+ * WordPress.com user data of the connected user.
+ *
+ * @var array
+ */
+ private $user_data;
+ /**
+ * WordPress.com username for the connected user.
+ *
+ * @var string
+ */
+ private $user_login;
+ /**
+ * WordPress.com email address for the connected user.
+ *
+ * @var string
+ */
+ private $user_email;
+ /**
+ * WordPress.com display name for the connected user.
+ *
+ * @var string
+ */
+ private $display_name;
+ /**
+ * Site URL sanitized for usage in WordPress.com slugs.
+ *
+ * @var string
+ */
+ private $primary_site_slug;
+ /**
+ * Text direction (ltr or rtl) based on connected WordPress.com user's interface settings.
+ *
+ * @var string
+ */
+ private $user_text_direction;
+ /**
+ * Number of sites owned by connected WordPress.com user.
+ *
+ * @var int
+ */
+ private $user_site_count;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->locale = $this->get_locale();
+ $this->user_id = get_current_user_id();
+
+ // Limit the masterbar to be shown only to connected Jetpack users.
+ if ( ! Jetpack::is_user_connected( $this->user_id ) ) {
+ return;
+ }
+
+ // Don't show the masterbar on WordPress mobile apps.
+ if ( Jetpack_User_Agent_Info::is_mobile_app() ) {
+ add_filter( 'show_admin_bar', '__return_false' );
+ return;
+ }
+
+ Jetpack::dns_prefetch(
+ array(
+ '//s0.wp.com',
+ '//s1.wp.com',
+ '//s2.wp.com',
+ '//0.gravatar.com',
+ '//1.gravatar.com',
+ '//2.gravatar.com',
+ )
+ );
+
+ // Atomic only.
+ if ( jetpack_is_atomic_site() ) {
+ /*
+ * override user setting that hides masterbar from site's front.
+ * https://github.com/Automattic/jetpack/issues/7667
+ */
+ add_filter( 'show_admin_bar', '__return_true' );
+ }
+
+ $this->user_data = Jetpack::get_connected_user_data( $this->user_id );
+ $this->user_login = $this->user_data['login'];
+ $this->user_email = $this->user_data['email'];
+ $this->display_name = $this->user_data['display_name'];
+ $this->user_site_count = $this->user_data['site_count'];
+
+ // Used to build menu links that point directly to Calypso.
+ $this->primary_site_slug = Jetpack::build_raw_urls( get_home_url() );
+
+ // Used for display purposes and for building WP Admin links.
+ $this->primary_site_url = str_replace( '::', '/', $this->primary_site_slug );
+
+ // We need to use user's setting here, instead of relying on current blog's text direction.
+ $this->user_text_direction = $this->user_data['text_direction'];
+
+ if ( $this->is_rtl() ) {
+ // Extend core WP_Admin_Bar class in order to add rtl styles.
+ add_filter( 'wp_admin_bar_class', array( $this, 'get_rtl_admin_bar_class' ) );
+ }
+ add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
+
+ add_action( 'wp_before_admin_bar_render', array( $this, 'replace_core_masterbar' ), 99999 );
+
+ add_action( 'wp_enqueue_scripts', array( $this, 'add_styles_and_scripts' ) );
+ add_action( 'admin_enqueue_scripts', array( $this, 'add_styles_and_scripts' ) );
+
+ add_action( 'wp_enqueue_scripts', array( $this, 'remove_core_styles' ) );
+ add_action( 'admin_enqueue_scripts', array( $this, 'remove_core_styles' ) );
+
+ if ( Jetpack::is_module_active( 'notes' ) && $this->is_rtl() ) {
+ // Override Notification module to include RTL styles.
+ add_action( 'a8c_wpcom_masterbar_enqueue_rtl_notification_styles', '__return_true' );
+ }
+ }
+
+ /**
+ * Get class name for RTL sites.
+ */
+ public function get_rtl_admin_bar_class() {
+ return 'RTL_Admin_Bar';
+ }
+
+ /**
+ * Adds CSS classes to admin body tag.
+ *
+ * @since 5.1
+ *
+ * @param string $admin_body_classes CSS classes that will be added.
+ *
+ * @return string
+ */
+ public function admin_body_class( $admin_body_classes ) {
+ return "$admin_body_classes jetpack-masterbar";
+ }
+
+ /**
+ * Remove the default Admin Bar CSS.
+ */
+ public function remove_core_styles() {
+ wp_dequeue_style( 'admin-bar' );
+ }
+
+ /**
+ * Check if the user settings are for an RTL language or not.
+ */
+ public function is_rtl() {
+ return 'rtl' === $this->user_text_direction ? true : false;
+ }
+
+ /**
+ * Enqueue our own CSS and JS to display our custom admin bar.
+ */
+ public function add_styles_and_scripts() {
+
+ if ( $this->is_rtl() ) {
+ wp_enqueue_style( 'a8c-wpcom-masterbar-rtl', $this->wpcom_static_url( '/wp-content/mu-plugins/admin-bar/rtl/wpcom-admin-bar-rtl.css' ), array(), JETPACK__VERSION );
+ wp_enqueue_style( 'a8c-wpcom-masterbar-overrides-rtl', $this->wpcom_static_url( '/wp-content/mu-plugins/admin-bar/masterbar-overrides/rtl/masterbar-rtl.css' ), array(), JETPACK__VERSION );
+ } else {
+ wp_enqueue_style( 'a8c-wpcom-masterbar', $this->wpcom_static_url( '/wp-content/mu-plugins/admin-bar/wpcom-admin-bar.css' ), array(), JETPACK__VERSION );
+ wp_enqueue_style( 'a8c-wpcom-masterbar-overrides', $this->wpcom_static_url( '/wp-content/mu-plugins/admin-bar/masterbar-overrides/masterbar.css' ), array(), JETPACK__VERSION );
+ }
+
+ // Local overrides.
+ wp_enqueue_style( 'a8c_wpcom_css_override', plugins_url( 'overrides.css', __FILE__ ), array(), JETPACK__VERSION );
+
+ if ( ! Jetpack::is_module_active( 'notes ' ) ) {
+ // Masterbar is relying on some icons from noticons.css.
+ wp_enqueue_style( 'noticons', $this->wpcom_static_url( '/i/noticons/noticons.css' ), array(), JETPACK__VERSION . '-' . gmdate( 'oW' ) );
+ }
+
+ wp_enqueue_script(
+ 'jetpack-accessible-focus',
+ Jetpack::get_file_url_for_environment( '_inc/build/accessible-focus.min.js', '_inc/accessible-focus.js' ),
+ array(),
+ JETPACK__VERSION,
+ false
+ );
+ wp_enqueue_script(
+ 'a8c_wpcom_masterbar_tracks_events',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/masterbar/tracks-events.min.js',
+ 'modules/masterbar/tracks-events.js'
+ ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ false
+ );
+
+ wp_enqueue_script(
+ 'a8c_wpcom_masterbar_overrides',
+ $this->wpcom_static_url( '/wp-content/mu-plugins/admin-bar/masterbar-overrides/masterbar.js' ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ false
+ );
+ }
+
+ /**
+ * Get base URL where our CSS and JS will come from.
+ *
+ * @param string $file File path for a static resource.
+ */
+ private function wpcom_static_url( $file ) {
+ if ( ! empty( $this->sandbox_url ) ) {
+ // For testing undeployed changes to remotely enqueued scripts and styles.
+ return set_url_scheme( $this->sandbox_url . $file, 'https' );
+ }
+
+ $i = hexdec( substr( md5( $file ), - 1 ) ) % 2;
+ $url = 'https://s' . $i . '.wp.com' . $file;
+
+ return set_url_scheme( $url, 'https' );
+ }
+
+ /**
+ * Remove the default admin bar items and replace it with our own admin bar.
+ */
+ public function replace_core_masterbar() {
+ global $wp_admin_bar;
+
+ if ( ! is_object( $wp_admin_bar ) ) {
+ return false;
+ }
+
+ $this->clear_core_masterbar( $wp_admin_bar );
+ $this->build_wpcom_masterbar( $wp_admin_bar );
+ }
+
+ /**
+ * Remove all existing toolbar entries from core Masterbar
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin Bar instance.
+ */
+ public function clear_core_masterbar( $wp_admin_bar ) {
+ foreach ( $wp_admin_bar->get_nodes() as $node ) {
+ $wp_admin_bar->remove_node( $node->id );
+ }
+ }
+
+ /**
+ * Add entries corresponding to WordPress.com Masterbar
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin Bar instance.
+ */
+ public function build_wpcom_masterbar( $wp_admin_bar ) {
+ // Menu groups.
+ $this->wpcom_adminbar_add_secondary_groups( $wp_admin_bar );
+
+ // Left part.
+ $this->add_my_sites_submenu( $wp_admin_bar );
+ $this->add_reader_submenu( $wp_admin_bar );
+
+ // Right part.
+ if ( Jetpack::is_module_active( 'notes' ) ) {
+ $this->add_notifications( $wp_admin_bar );
+ }
+
+ $this->add_me_submenu( $wp_admin_bar );
+ $this->add_write_button( $wp_admin_bar );
+
+ // Add a sidebar toggle on mobile.
+ wp_admin_bar_sidebar_toggle( $wp_admin_bar );
+ }
+
+ /**
+ * Get WordPress.com current locale name.
+ */
+ public function get_locale() {
+ $wpcom_locale = get_locale();
+
+ if ( ! class_exists( 'GP_Locales' ) ) {
+ if ( defined( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) && file_exists( JETPACK__GLOTPRESS_LOCALES_PATH ) ) {
+ require JETPACK__GLOTPRESS_LOCALES_PATH;
+ }
+ }
+
+ if ( class_exists( 'GP_Locales' ) ) {
+ $wpcom_locale_object = GP_Locales::by_field( 'wp_locale', get_locale() );
+ if ( $wpcom_locale_object instanceof GP_Locale ) {
+ $wpcom_locale = $wpcom_locale_object->slug;
+ }
+ }
+
+ return $wpcom_locale;
+ }
+
+ /**
+ * Add the Notifications menu item.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin Bar instance.
+ */
+ public function add_notifications( $wp_admin_bar ) {
+ $wp_admin_bar->add_node(
+ array(
+ 'id' => 'notes',
+ 'title' => '<span id="wpnt-notes-unread-count" class="wpnt-loading wpn-read"></span>
+ <span class="screen-reader-text">' . esc_html__( 'Notifications', 'jetpack' ) . '</span>
+ <span class="noticon noticon-bell"></span>',
+ 'meta' => array(
+ 'html' => '<div id="wpnt-notes-panel2" style="display:none" lang="' . esc_attr( $this->locale ) . '" dir="' . ( $this->is_rtl() ? 'rtl' : 'ltr' ) . '">' .
+ '<div class="wpnt-notes-panel-header">' .
+ '<span class="wpnt-notes-header">' .
+ esc_html__( 'Notifications', 'jetpack' ) .
+ '</span>' .
+ '<span class="wpnt-notes-panel-link">' .
+ '</span>' .
+ '</div>' .
+ '</div>',
+ 'class' => 'menupop mb-trackable',
+ ),
+ 'parent' => 'top-secondary',
+ )
+ );
+ }
+
+ /**
+ * Add the "My Site" menu item in the root default group.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin Bar instance.
+ */
+ public function add_my_sites_submenu( $wp_admin_bar ) {
+ $wp_admin_bar->add_menu(
+ array(
+ 'parent' => 'root-default',
+ 'id' => 'blog',
+ 'title' => _n( 'My Site', 'My Sites', $this->user_site_count, 'jetpack' ),
+ 'href' => 'https://wordpress.com/stats/' . esc_attr( $this->primary_site_slug ),
+ 'meta' => array(
+ 'class' => 'my-sites mb-trackable',
+ ),
+ )
+ );
+ }
+
+ /**
+ * Add the "Reader" menu item in the root default group.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin Bar instance.
+ */
+ public function add_reader_submenu( $wp_admin_bar ) {
+ $wp_admin_bar->add_menu(
+ array(
+ 'parent' => 'root-default',
+ 'id' => 'newdash',
+ 'title' => esc_html__( 'Reader', 'jetpack' ),
+ 'href' => 'https://wordpress.com/',
+ 'meta' => array(
+ 'class' => 'mb-trackable',
+ ),
+ )
+ );
+ }
+
+ /**
+ * Define main groups used in our admin bar.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin Bar instance.
+ */
+ public function wpcom_adminbar_add_secondary_groups( $wp_admin_bar ) {
+ $wp_admin_bar->add_group(
+ array(
+ 'id' => 'root-default',
+ 'meta' => array(
+ 'class' => 'ab-top-menu',
+ ),
+ )
+ );
+
+ $wp_admin_bar->add_group(
+ array(
+ 'id' => 'top-secondary',
+ 'meta' => array(
+ 'class' => 'ab-top-secondary',
+ ),
+ )
+ );
+ }
+
+ /**
+ * Add User info menu item.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin Bar instance.
+ */
+ public function add_me_submenu( $wp_admin_bar ) {
+ $user_id = get_current_user_id();
+ if ( empty( $user_id ) ) {
+ return;
+ }
+
+ $avatar = get_avatar( $this->user_email, 32, 'mm', '', array( 'force_display' => true ) );
+ $class = empty( $avatar ) ? 'mb-trackable' : 'with-avatar mb-trackable';
+
+ // Add the 'Me' menu.
+ $wp_admin_bar->add_menu(
+ array(
+ 'id' => 'my-account',
+ 'parent' => 'top-secondary',
+ 'title' => $avatar . '<span class="ab-text">' . esc_html__( 'Me', 'jetpack' ) . '</span>',
+ 'href' => 'https://wordpress.com/me/account',
+ 'meta' => array(
+ 'class' => $class,
+ ),
+ )
+ );
+ }
+
+ /**
+ * Add Write Menu item.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin Bar instance.
+ */
+ public function add_write_button( $wp_admin_bar ) {
+ $current_user = wp_get_current_user();
+
+ $posting_blog_id = get_current_blog_id();
+ if ( ! is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
+ $posting_blog_id = $current_user->primary_blog;
+ }
+
+ $user_can_post = current_user_can_for_blog( $posting_blog_id, 'publish_posts' );
+
+ if ( ! $posting_blog_id || ! $user_can_post ) {
+ return;
+ }
+
+ $blog_post_page = 'https://wordpress.com/post/' . esc_attr( $this->primary_site_slug );
+
+ $wp_admin_bar->add_menu(
+ array(
+ 'parent' => 'top-secondary',
+ 'id' => 'ab-new-post',
+ 'href' => $blog_post_page,
+ 'title' => '<span>' . esc_html__( 'Write', 'jetpack' ) . '</span>',
+ 'meta' => array(
+ 'class' => 'mb-trackable',
+ ),
+ )
+ );
+ }
+}
diff --git a/plugins/jetpack/modules/masterbar/overrides.css b/plugins/jetpack/modules/masterbar/overrides.css
new file mode 100644
index 00000000..a1c9a534
--- /dev/null
+++ b/plugins/jetpack/modules/masterbar/overrides.css
@@ -0,0 +1,61 @@
+/* Remove min-height from menu elements that was causing them to render incorrectly */
+.my-sites li {
+ min-height: unset !important;
+}
+
+/* Overwrite a core style which breaks the overflow for .my-sites in Safari */
+#wpadminbar li.menupop.my-sites {
+ overflow: visible;
+}
+
+/* Add a focus style for menu items */
+.accessible-focus #wpadminbar li.menupop a.ab-item:focus,
+.accessible-focus #wpadminbar li#wp-admin-bar-notes.menupop .ab-item:focus,
+.accessible-focus #wpadminbar ul li#wp-admin-bar-ab-new-post a:focus {
+ -webkit-box-shadow: inset 2px 2px 0 #668eaa,
+ inset -2px -2px 0 #668eaa;
+ box-shadow: inset 2px 2px 0 #668eaa,
+ inset -2px -2px 0 #668eaa;
+}
+
+/* Menu items in panels are inside `ab-empty-item` */
+.accessible-focus #wpadminbar li.menupop .ab-empty-item a.ab-item:focus,
+.accessible-focus #wpadminbar li.menupop .ab-empty-item a.ab-secondary:focus,
+.accessible-focus #wpadminbar li.menupop .ab-empty-item a.username:focus {
+ -webkit-box-shadow: inset 2px 2px 0 #2e4354,
+ inset -2px -2px 0 #2e4354;
+ box-shadow: inset 2px 2px 0 #2e4354,
+ inset -2px -2px 0 #2e4354;
+}
+
+.accessible-focus #wpadminbar .quicklinks li#wp-admin-bar-my-account #wp-admin-bar-user-info .ab-sign-out:focus {
+ -webkit-box-shadow: inset 2px 2px 0 #2e4354,
+ inset -2px -2px 0 #2e4354 !important;
+ box-shadow: inset 2px 2px 0 #2e4354,
+ inset -2px -2px 0 #2e4354 !important;
+}
+
+.accessible-focus #wpadminbar:not(.mobile) .ab-top-menu > li > .ab-item:focus {
+ background: transparent;
+}
+
+/* Hide the panels initially */
+#wpadminbar li#wp-admin-bar-blog.menupop > .ab-sub-wrapper, /* My Sites */
+#wpadminbar li#wp-admin-bar-newdash.menupop > .ab-sub-wrapper, /* Reader */
+#wpadminbar li#wp-admin-bar-my-account.menupop > .ab-sub-wrapper, /* Me */
+#wpadminbar li#wp-admin-bar-notes.menupop > #wpnt-notes-panel2 { /* Notifications */
+ display: block !important;
+}
+
+/* Change notification icon the match the one on WP.com */
+#wp-admin-bar-notes .noticon-bell:before {
+ content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cmVjdCB4PSIwIiBmaWxsPSJub25lIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiLz48Zz48cGF0aCBmaWxsPSIjZmZmZmZmIiBkPSJNNi4xNCAxNC45N2wyLjgyOCAyLjgyN2MtLjM2Mi4zNjItLjg2Mi41ODYtMS40MTQuNTg2LTEuMTA1IDAtMi0uODk1LTItMiAwLS41NTIuMjI0LTEuMDUyLjU4Ni0xLjQxNHptOC44NjcgNS4zMjRMMTQuMyAyMSAzIDkuN2wuNzA2LS43MDcgMS4xMDIuMTU3Yy43NTQuMTA4IDEuNjktLjEyMiAyLjA3Ny0uNTFsMy44ODUtMy44ODRjMi4zNC0yLjM0IDYuMTM1LTIuMzQgOC40NzUgMHMyLjM0IDYuMTM1IDAgOC40NzVsLTMuODg1IDMuODg2Yy0uMzg4LjM4OC0uNjE4IDEuMzIzLS41MSAyLjA3N2wuMTU3IDEuMXoiLz48L2c+PC9zdmc+") !important;
+}
+#wp-admin-bar-notes.active .noticon-bell:before {
+ content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cmVjdCB4PSIwIiBmaWxsPSJub25lIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiLz48Zz48cGF0aCBmaWxsPSIjMjMyODJkIiBkPSJNNi4xNCAxNC45N2wyLjgyOCAyLjgyN2MtLjM2Mi4zNjItLjg2Mi41ODYtMS40MTQuNTg2LTEuMTA1IDAtMi0uODk1LTItMiAwLS41NTIuMjI0LTEuMDUyLjU4Ni0xLjQxNHptOC44NjcgNS4zMjRMMTQuMyAyMSAzIDkuN2wuNzA2LS43MDcgMS4xMDIuMTU3Yy43NTQuMTA4IDEuNjktLjEyMiAyLjA3Ny0uNTFsMy44ODUtMy44ODRjMi4zNC0yLjM0IDYuMTM1LTIuMzQgOC40NzUgMHMyLjM0IDYuMTM1IDAgOC40NzVsLTMuODg1IDMuODg2Yy0uMzg4LjM4OC0uNjE4IDEuMzIzLS41MSAyLjA3N2wuMTU3IDEuMXoiLz48L2c+PC9zdmc+") !important;
+}
+
+/* Fit width of sign out button to content */
+#wpadminbar .quicklinks li#wp-admin-bar-my-account #wp-admin-bar-user-info .ab-sign-out {
+ display: inline-block;
+}
diff --git a/plugins/jetpack/modules/masterbar/rtl-admin-bar.php b/plugins/jetpack/modules/masterbar/rtl-admin-bar.php
new file mode 100644
index 00000000..539aa3fe
--- /dev/null
+++ b/plugins/jetpack/modules/masterbar/rtl-admin-bar.php
@@ -0,0 +1,55 @@
+<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+
+if ( ! class_exists( 'WP_Admin_Bar' ) ) {
+ require_once ABSPATH . '/wp-includes/class-wp-admin-bar.php';
+}
+
+/**
+ * We are using this class to replace core WP_Admin_Bar in cases when
+ * we need to override the default styles with rtl ones. This is
+ * achieved by adding 'rtl' class to #wpadminbar div. Apart from that
+ * the output of render method should be the same as the one of base class.
+ */
+class RTL_Admin_Bar extends WP_Admin_Bar {
+ /**
+ * Display the admin bar.
+ */
+ public function render() {
+ global $is_IE;
+ $root = $this->_bind();
+
+ // Add browser and RTL classes.
+ // We have to do this here since admin bar shows on the front end.
+ $class = 'nojq nojs rtl';
+ if ( $is_IE ) {
+ if ( strpos( $_SERVER['HTTP_USER_AGENT'], 'MSIE 7' ) ) {
+ $class .= ' ie7';
+ } elseif ( strpos( $_SERVER['HTTP_USER_AGENT'], 'MSIE 8' ) ) {
+ $class .= ' ie8';
+ } elseif ( strpos( $_SERVER['HTTP_USER_AGENT'], 'MSIE 9' ) ) {
+ $class .= ' ie9';
+ }
+ } elseif ( wp_is_mobile() ) {
+ $class .= ' mobile';
+ }
+
+ ?>
+ <div id="wpadminbar" class="<?php echo esc_attr( $class ); ?>">
+ <?php if ( ! is_admin() ) : ?>
+ <a class="screen-reader-shortcut" href="#wp-toolbar" tabindex="1"><?php esc_html_e( 'Skip to toolbar', 'jetpack' ); ?></a>
+ <?php endif; ?>
+ <div class="quicklinks" id="wp-toolbar" role="navigation" aria-label="<?php esc_attr_e( 'Toolbar', 'jetpack' ); ?>" tabindex="0">
+ <?php
+ foreach ( $root->children as $group ) :
+ $this->_render_group( $group );
+ endforeach;
+ ?>
+ </div>
+ <?php if ( is_user_logged_in() ) : ?>
+ <a class="screen-reader-shortcut" href="<?php echo esc_url( wp_logout_url() ); ?>"><?php esc_html_e( 'Log Out', 'jetpack' ); ?></a>
+ <?php endif; ?>
+ </div>
+
+ <?php
+ }
+}
diff --git a/plugins/jetpack/modules/masterbar/tracks-events.js b/plugins/jetpack/modules/masterbar/tracks-events.js
new file mode 100644
index 00000000..8364266c
--- /dev/null
+++ b/plugins/jetpack/modules/masterbar/tracks-events.js
@@ -0,0 +1,110 @@
+/*globals jQuery, JSON */
+( function( $ ) {
+ var eventName = 'masterbar_click';
+
+ var linksTracksEvents = {
+ //top level items
+ 'wp-admin-bar-blog': 'my_sites',
+ 'wp-admin-bar-newdash': 'reader',
+ 'wp-admin-bar-ab-new-post': 'write_button',
+ 'wp-admin-bar-my-account': 'my_account',
+ 'wp-admin-bar-notes': 'notifications',
+ };
+
+ var notesTracksEvents = {
+ openSite: function( data ) {
+ return {
+ clicked: 'masterbar_notifications_panel_site',
+ site_id: data.siteId,
+ };
+ },
+ openPost: function( data ) {
+ return {
+ clicked: 'masterbar_notifications_panel_post',
+ site_id: data.siteId,
+ post_id: data.postId,
+ };
+ },
+ openComment: function( data ) {
+ return {
+ clicked: 'masterbar_notifications_panel_comment',
+ site_id: data.siteId,
+ post_id: data.postId,
+ comment_id: data.commentId,
+ };
+ },
+ };
+
+ function parseJson( s, defaultValue ) {
+ try {
+ return JSON.parse( s );
+ } catch ( e ) {
+ return defaultValue;
+ }
+ }
+
+ $( document ).ready( function() {
+ var trackableLinks =
+ '.mb-trackable .ab-item:not(div),' +
+ '#wp-admin-bar-notes .ab-item,' +
+ '#wp-admin-bar-user-info .ab-item,' +
+ '.mb-trackable .ab-secondary';
+
+ $( trackableLinks ).on( 'click touchstart', function( e ) {
+ if ( ! window.jpTracksAJAX || 'function' !== typeof window.jpTracksAJAX.record_ajax_event ) {
+ return;
+ }
+
+ var $target = $( e.target ),
+ $parent = $target.closest( 'li' );
+
+ if ( ! $target.is( 'a' ) ) {
+ $target = $target.closest( 'a' );
+ }
+
+ if ( ! $parent || ! $target ) {
+ return;
+ }
+
+ var trackingId = $target.attr( 'ID' ) || $parent.attr( 'ID' );
+
+ if ( ! linksTracksEvents.hasOwnProperty( trackingId ) ) {
+ return;
+ }
+ var eventProps = { clicked: linksTracksEvents[ trackingId ] };
+
+ if ( $parent.hasClass( 'menupop' ) ) {
+ window.jpTracksAJAX.record_ajax_event( eventName, 'click', eventProps );
+ } else {
+ e.preventDefault();
+ window.jpTracksAJAX.record_ajax_event( eventName, 'click', eventProps ).always( function() {
+ window.location = $target.attr( 'href' );
+ } );
+ }
+ } );
+ } );
+
+ // listen for postMessage events from the notifications iframe
+ $( window ).on( 'message', function( e ) {
+ if ( ! window.jpTracksAJAX || 'function' !== typeof window.jpTracksAJAX.record_ajax_event ) {
+ return;
+ }
+
+ var event = ! e.data && e.originalEvent.data ? e.originalEvent : e;
+ if ( event.origin !== 'https://widgets.wp.com' ) {
+ return;
+ }
+
+ var data = 'string' === typeof event.data ? parseJson( event.data, {} ) : event.data;
+ if ( 'notesIframeMessage' !== data.type ) {
+ return;
+ }
+
+ var eventData = notesTracksEvents[ data.action ];
+ if ( ! eventData ) {
+ return;
+ }
+
+ window.jpTracksAJAX.record_ajax_event( eventName, 'click', eventData( data ) );
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/memberships/class-jetpack-memberships.php b/plugins/jetpack/modules/memberships/class-jetpack-memberships.php
new file mode 100644
index 00000000..989d4e96
--- /dev/null
+++ b/plugins/jetpack/modules/memberships/class-jetpack-memberships.php
@@ -0,0 +1,272 @@
+<?php
+/**
+ * Jetpack_Memberships: wrapper for memberships functions.
+ *
+ * @package Jetpack
+ * @since 7.3.0
+ */
+
+/**
+ * Class Jetpack_Memberships
+ * This class represents the Memberships functionality.
+ */
+class Jetpack_Memberships {
+ /**
+ * CSS class prefix to use in the styling.
+ *
+ * @var string
+ */
+ public static $css_classname_prefix = 'jetpack-memberships';
+ /**
+ * Our CPT type for the product (plan).
+ *
+ * @var string
+ */
+ public static $post_type_plan = 'jp_mem_plan';
+ /**
+ * Option that will store currently set up account (Stripe etc) id for memberships.
+ *
+ * @var string
+ */
+ public static $connected_account_id_option_name = 'jetpack-memberships-connected-account-id';
+ /**
+ * Button block type to use.
+ *
+ * @var string
+ */
+ private static $button_block_name = 'membership-button';
+ /**
+ * Classic singleton pattern
+ *
+ * @var Jetpack_Memberships
+ */
+ private static $instance;
+
+ /**
+ * Jetpack_Memberships constructor.
+ */
+ private function __construct() {}
+
+ /**
+ * The actual constructor initializing the object.
+ *
+ * @return Jetpack_Memberships
+ */
+ public static function get_instance() {
+ if ( ! self::$instance ) {
+ self::$instance = new self();
+ self::$instance->register_init_hook();
+ }
+
+ return self::$instance;
+ }
+ /**
+ * Get the map that defines the shape of CPT post. keys are names of fields and
+ * 'meta' is the name of actual WP post meta field that corresponds.
+ *
+ * @return array
+ */
+ private static function get_plan_property_mapping() {
+ $meta_prefix = 'jetpack_memberships_';
+ $properties = array(
+ 'price' => array(
+ 'meta' => $meta_prefix . 'price',
+ ),
+ 'currency' => array(
+ 'meta' => $meta_prefix . 'currency',
+ ),
+ );
+ return $properties;
+ }
+
+ /**
+ * Inits further hooks on init hook.
+ */
+ private function register_init_hook() {
+ add_action( 'init', array( $this, 'init_hook_action' ) );
+ }
+
+ /**
+ * Actual hooks initializing on init.
+ */
+ public function init_hook_action() {
+ add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
+ add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
+ $this->setup_cpts();
+ }
+
+ /**
+ * Sets up the custom post types for the module.
+ */
+ private function setup_cpts() {
+ /*
+ * PLAN data structure.
+ */
+ $capabilities = array(
+ 'edit_post' => 'edit_posts',
+ 'read_post' => 'read_private_posts',
+ 'delete_post' => 'delete_posts',
+ 'edit_posts' => 'edit_posts',
+ 'edit_others_posts' => 'edit_others_posts',
+ 'publish_posts' => 'publish_posts',
+ 'read_private_posts' => 'read_private_posts',
+ );
+ $order_args = array(
+ 'label' => esc_html__( 'Plan', 'jetpack' ),
+ 'description' => esc_html__( 'Memberships plans', 'jetpack' ),
+ 'supports' => array( 'title', 'custom-fields', 'content' ),
+ 'hierarchical' => false,
+ 'public' => false,
+ 'show_ui' => false,
+ 'show_in_menu' => false,
+ 'show_in_admin_bar' => false,
+ 'show_in_nav_menus' => false,
+ 'can_export' => true,
+ 'has_archive' => false,
+ 'exclude_from_search' => true,
+ 'publicly_queryable' => false,
+ 'rewrite' => false,
+ 'capabilities' => $capabilities,
+ 'show_in_rest' => false,
+ );
+ register_post_type( self::$post_type_plan, $order_args );
+ }
+
+ /**
+ * Allows custom post types to be used by REST API.
+ *
+ * @param array $post_types - other post types.
+ *
+ * @see hook 'rest_api_allowed_post_types'
+ * @return array
+ */
+ public function allow_rest_api_types( $post_types ) {
+ $post_types[] = self::$post_type_plan;
+
+ return $post_types;
+ }
+
+ /**
+ * Allows custom meta fields to sync.
+ *
+ * @param array $post_meta - previously changet post meta.
+ *
+ * @return array
+ */
+ public function allow_sync_post_meta( $post_meta ) {
+ $meta_keys = array_map(
+ array( $this, 'return_meta' ),
+ $this->get_plan_property_mapping()
+ );
+ return array_merge( $post_meta, array_values( $meta_keys ) );
+ }
+
+ /**
+ * This returns meta attribute of passet array.
+ * Used for array functions.
+ *
+ * @param array $map - stuff.
+ *
+ * @return mixed
+ */
+ public function return_meta( $map ) {
+ return $map['meta'];
+ }
+ /**
+ * Callback that parses the membership purchase shortcode.
+ *
+ * @param array $attrs - attributes in the shortcode. `id` here is the CPT id of the plan.
+ *
+ * @return string|void
+ */
+ public function render_button( $attrs ) {
+ Jetpack_Gutenberg::load_assets_as_required( self::$button_block_name, array( 'thickbox', 'wp-polyfill' ) );
+
+ if ( empty( $attrs['planId'] ) ) {
+ return;
+ }
+ $id = intval( $attrs['planId'] );
+ $product = get_post( $id );
+ if ( ! $product || is_wp_error( $product ) ) {
+ return;
+ }
+ if ( $product->post_type !== self::$post_type_plan || 'publish' !== $product->post_status ) {
+ return;
+ }
+
+ $data = array(
+ 'blog_id' => self::get_blog_id(),
+ 'id' => $id,
+ 'button_label' => __( 'Your contribution', 'jetpack' ),
+ 'powered_text' => __( 'Powered by WordPress.com', 'jetpack' ),
+ );
+
+ $classes = array(
+ 'components-button',
+ 'is-primary',
+ 'is-button',
+ 'wp-block-jetpack-' . self::$button_block_name,
+ self::$css_classname_prefix . '-' . $data['id'],
+ );
+ if ( isset( $attrs['className'] ) ) {
+ array_push( $classes, $attrs['className'] );
+ }
+ if ( isset( $attrs['submitButtonText'] ) ) {
+ $data['button_label'] = $attrs['submitButtonText'];
+ }
+ $button_styles = array();
+ if ( ! empty( $attrs['customBackgroundButtonColor'] ) ) {
+ array_push(
+ $button_styles,
+ sprintf(
+ 'background-color: %s',
+ sanitize_hex_color( $attrs['customBackgroundButtonColor'] )
+ )
+ );
+ }
+ if ( ! empty( $attrs['customTextButtonColor'] ) ) {
+ array_push(
+ $button_styles,
+ sprintf(
+ 'color: %s',
+ sanitize_hex_color( $attrs['customTextButtonColor'] )
+ )
+ );
+ }
+ $button_styles = implode( $button_styles, ';' );
+ add_thickbox();
+ return sprintf(
+ '<button data-blog-id="%d" data-powered-text="%s" data-plan-id="%d" data-lang="%s" class="%s" style="%s">%s</button>',
+ esc_attr( $data['blog_id'] ),
+ esc_attr( $data['powered_text'] ),
+ esc_attr( $data['id'] ),
+ esc_attr( get_locale() ),
+ esc_attr( implode( $classes, ' ' ) ),
+ esc_attr( $button_styles ),
+ esc_html( $data['button_label'] )
+ );
+ }
+
+ /**
+ * Get current blog id.
+ *
+ * @return int
+ */
+ public static function get_blog_id() {
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ return get_current_blog_id();
+ }
+
+ return Jetpack_Options::get_option( 'id' );
+ }
+
+ /**
+ * Get the id of the connected payment acount (Stripe etc).
+ *
+ * @return int|void
+ */
+ public static function get_connected_account_id() {
+ return get_option( self::$connected_account_id_option_name );
+ }
+}
+Jetpack_Memberships::get_instance();
diff --git a/plugins/jetpack/modules/minileven.php b/plugins/jetpack/modules/minileven.php
new file mode 100644
index 00000000..0a12d167
--- /dev/null
+++ b/plugins/jetpack/modules/minileven.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * Module Name: Mobile Theme
+ * Module Description: Enable the Jetpack Mobile theme
+ * Sort Order: 21
+ * Recommendation Order: 11
+ * First Introduced: 1.8
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Appearance, Mobile, Recommended
+ * Feature: Appearance
+ * Additional Search Queries: mobile, theme, minileven
+ */
+
+function jetpack_load_minileven() {
+ include dirname( __FILE__ ) . "/minileven/minileven.php";
+
+ if ( Jetpack_Options::get_option_and_ensure_autoload( 'wp_mobile_app_promos', '0' ) != '1' )
+ remove_action( 'wp_mobile_theme_footer', 'jetpack_mobile_app_promo' );
+}
+
+add_action( 'jetpack_modules_loaded', 'minileven_loaded' );
+
+function minileven_loaded() {
+ Jetpack::enable_module_configurable( __FILE__ );
+}
+
+function minileven_theme_root( $theme_root ) {
+ if ( jetpack_check_mobile() ) {
+ return dirname( __FILE__ ) . '/minileven/theme';
+ }
+
+ return $theme_root;
+}
+
+add_filter( 'theme_root', 'minileven_theme_root' );
+
+function minileven_theme_root_uri( $theme_root_uri ) {
+ if ( jetpack_check_mobile() ) {
+ return plugins_url( 'modules/minileven/theme', dirname( __FILE__ ) );
+ }
+
+ return $theme_root_uri;
+}
+
+add_filter( 'theme_root_uri', 'minileven_theme_root_uri' );
+
+function minileven_enabled( $wp_mobile_disable_option ) {
+ return true;
+}
+
+if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ add_filter( 'option_wp_mobile_disable', 'minileven_enabled' );
+}
+
+jetpack_load_minileven();
diff --git a/plugins/jetpack/modules/minileven/images/wp-app-devices.png b/plugins/jetpack/modules/minileven/images/wp-app-devices.png
new file mode 100644
index 00000000..2efefe20
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/images/wp-app-devices.png
Binary files differ
diff --git a/plugins/jetpack/modules/minileven/minileven.php b/plugins/jetpack/modules/minileven/minileven.php
new file mode 100644
index 00000000..abfec53d
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/minileven.php
@@ -0,0 +1,344 @@
+<?php
+
+// ********** modify blog option 'wp_mobile_template' manually to specify a theme (ex. 'vip/cnnmobile')
+
+// WordPress Mobile Edition
+//
+// Copyright (c) 2002-2008 Alex King
+// http://alexking.org/projects/wordpress
+//
+// Released under the GPL license
+// http://www.opensource.org/licenses/gpl-license.php
+//
+// **********************************************************************
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// *****************************************************************
+
+/*
+Plugin Name: WordPress Mobile Edition
+Plugin URI: http://alexking.org/projects/wordpress
+Description: Show a mobile view of the post/page if the visitor is on a known mobile device. Questions on configuration, etc.? Make sure to read the README.
+Author: Alex King
+Author URI: http://alexking.org
+Version: 2.1a-WPCOM
+*/
+
+$_SERVER['REQUEST_URI'] = ( isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $_SERVER['SCRIPT_NAME'] . (( isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '')));
+
+function jetpack_check_mobile() {
+ if ( ( defined('XMLRPC_REQUEST') && XMLRPC_REQUEST ) || ( defined('APP_REQUEST') && APP_REQUEST ) )
+ return false;
+ if ( !isset($_SERVER["HTTP_USER_AGENT"]) || (isset($_COOKIE['akm_mobile']) && $_COOKIE['akm_mobile'] == 'false') )
+ return false;
+ if ( jetpack_mobile_exclude() )
+ return false;
+ if ( 1 == Jetpack_Options::get_option_and_ensure_autoload( 'wp_mobile_disable', '0' ) )
+ return false;
+ if ( isset($_COOKIE['akm_mobile']) && $_COOKIE['akm_mobile'] == 'true' )
+ return true;
+
+ $is_mobile = jetpack_is_mobile();
+
+ /**
+ * Filter the Mobile check results.
+ *
+ * @module minileven
+ *
+ * @since 1.8.0
+ *
+ * @param bool $is_mobile Is the reader on a mobile device.
+ */
+ return apply_filters( 'jetpack_check_mobile', $is_mobile );
+}
+
+function jetpack_mobile_exclude() {
+ $exclude = false;
+ $pages_to_exclude = array(
+ 'wp-admin',
+ 'wp-comments-post.php',
+ 'wp-mail.php',
+ 'wp-login.php',
+ 'wp-activate.php',
+ );
+ foreach ( $pages_to_exclude as $exclude_page ) {
+ if ( strstr( strtolower( $_SERVER['REQUEST_URI'] ), $exclude_page ) )
+ $exclude = true;
+ }
+
+ if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX )
+ $exclude = false;
+
+ if ( isset( $GLOBALS['wp_customize'] ) )
+ return true;
+
+ return $exclude;
+}
+
+function wp_mobile_get_main_template() {
+ remove_action( 'option_template', 'jetpack_mobile_template' );
+ $template = get_option( 'template' );
+ add_action( 'option_template', 'jetpack_mobile_template' );
+ return $template;
+}
+
+function wp_mobile_get_main_stylesheet() {
+ remove_action( 'option_stylesheet', 'jetpack_mobile_stylesheet' );
+ $stylesheet = get_option( 'stylesheet' );
+ add_action( 'option_stylesheet', 'jetpack_mobile_stylesheet' );
+ return $stylesheet;
+}
+
+function jetpack_mobile_stylesheet( $theme ) {
+ /**
+ * Filter Jetpack's Mobile stylesheet.
+ *
+ * @module minileven
+ *
+ * @since 1.8.0
+ *
+ * @param string $theme Theme.
+ */
+ return apply_filters( 'jetpack_mobile_stylesheet', 'pub/minileven', $theme );
+}
+
+function jetpack_mobile_template( $theme ) {
+ /**
+ * Filter Jetpack's Mobile template.
+ *
+ * @module minileven
+ *
+ * @since 1.8.0
+ *
+ * @param string $theme Theme.
+ */
+ return apply_filters( 'jetpack_mobile_template', 'pub/minileven', $theme );
+}
+
+function jetpack_mobile_available() {
+ echo '<div class="jetpack-mobile-link" style="text-align:center;margin:10px 0;"><a href="'. esc_url( home_url( add_query_arg('ak_action', 'accept_mobile') ) ) . '">' . __( 'View Mobile Site', 'jetpack' ) . '</a></div>';
+}
+
+function jetpack_mobile_request_handler() {
+ global $wpdb;
+ if (isset($_GET['ak_action'])) {
+ $url = parse_url( get_bloginfo( 'url' ) );
+ $domain = $url['host'];
+ if (!empty($url['path'])) {
+ $path = $url['path'];
+ }
+ else {
+ $path = '/';
+ }
+ $redirect = false;
+ switch ($_GET['ak_action']) {
+ case 'reject_mobile':
+ setcookie(
+ 'akm_mobile'
+ , 'false'
+ , time() + 300000
+ , $path
+ , $domain
+ );
+ $redirect = true;
+
+ /**
+ * In Jetpack's Mobile theme, fires after the user taps on the link to display a full version of the site.
+ *
+ * @module minileven
+ *
+ * @since 1.8.0
+ */
+ do_action( 'mobile_reject_mobile' );
+ break;
+ case 'force_mobile':
+ case 'accept_mobile':
+ setcookie(
+ 'akm_mobile'
+ , 'true'
+ , time() + 300000
+ , $path
+ , $domain
+ );
+ $redirect = true;
+
+ /**
+ * In Jetpack's Mobile theme, fires after the user taps on the link to go back from full site to mobile site.
+ *
+ * @module minileven
+ *
+ * @since 1.8.0
+ */
+ do_action( 'mobile_force_mobile' );
+ break;
+ }
+ if ($redirect) {
+ if ( isset( $_GET['redirect_to'] ) && $_GET['redirect_to'] ) {
+ $go = urldecode( $_GET['redirect_to'] );
+ } else {
+ $go = remove_query_arg( array( 'ak_action' ) );
+ }
+ wp_safe_redirect( $go );
+ exit;
+ }
+ }
+}
+add_action('init', 'jetpack_mobile_request_handler');
+
+function jetpack_mobile_theme_setup() {
+ if ( jetpack_check_mobile() ) {
+ // Redirect to download page if user clicked mobile app promo link in mobile footer
+ if ( isset( $_GET['app-download'] ) ) {
+ /**
+ * Fires before you're redirected to download page if you clicked the mobile app promo link in mobile footer
+ *
+ * @module minileven
+ *
+ * @since 1.8.0
+ *
+ * @param string $_GET['app-download'] app-download URL parameter.
+ */
+ do_action( 'mobile_app_promo_download', $_GET['app-download'] );
+
+ switch ( $_GET['app-download'] ) {
+ case 'android':
+ header( 'Location: market://search?q=pname:org.wordpress.android' );
+ exit;
+ break;
+ case 'ios':
+ header( 'Location: http://itunes.apple.com/us/app/wordpress/id335703880?mt=8' );
+ exit;
+ break;
+ case 'blackberry':
+ header( 'Location: http://blackberry.wordpress.org/download/' );
+ exit;
+ break;
+ }
+ }
+
+ add_action('stylesheet', 'jetpack_mobile_stylesheet');
+ add_action('template', 'jetpack_mobile_template');
+ add_action('option_template', 'jetpack_mobile_template');
+ add_action('option_stylesheet', 'jetpack_mobile_stylesheet');
+
+ if ( class_exists( 'Jetpack_Custom_CSS' ) && method_exists( 'Jetpack_Custom_CSS', 'disable' ) && ! get_option( 'wp_mobile_custom_css' ) )
+ add_action( 'init', array( 'Jetpack_Custom_CSS', 'disable' ), 11 );
+
+ /**
+ * Fires after Jetpack's mobile theme has been setup.
+ *
+ * @module minileven
+ *
+ * @since 1.8.0
+ */
+ do_action( 'mobile_setup' );
+ }
+}
+
+// Need a hook after plugins_loaded (since this code won't be loaded in Jetpack
+// until then) but after init (because it has its own init hooks to add).
+add_action( 'setup_theme', 'jetpack_mobile_theme_setup' );
+
+if (isset($_COOKIE['akm_mobile']) && $_COOKIE['akm_mobile'] == 'false') {
+ add_action('wp_footer', 'jetpack_mobile_available');
+}
+
+function jetpack_mobile_app_promo() {
+ ?>
+ <script type="text/javascript">
+ if ( ! navigator.userAgent.match( /wp-(iphone|android|blackberry|nokia|windowsphone)/i ) ) {
+ if ( ( navigator.userAgent.match( /iphone/i ) ) || ( navigator.userAgent.match( /ipod/i ) ) )
+ document.write( '<span id="wpcom-mobile-app-promo" style="margin-top: 10px; font-size: 13px;"><strong>Now Available!</strong> <a href="/index.php?app-download=ios">Download WordPress for iOS</a></span><br /><br />' );
+ else if ( ( navigator.userAgent.match( /android/i ) ) && ( null == navigator.userAgent.match( /playbook/i ) && null == navigator.userAgent.match( /bb10/i ) ) )
+ document.write( '<span id="wpcom-mobile-app-promo" style="margin-top: 10px; font-size: 13px;"><strong>Now Available!</strong> <a href="/index.php?app-download=android">Download WordPress for Android</a></span><br /><br />' );
+ else if ( ( navigator.userAgent.match( /blackberry/i ) ) || ( navigator.userAgent.match( /playbook/i ) ) || ( navigator.userAgent.match( /bb10/i ) ) )
+ document.write( '<span id="wpcom-mobile-app-promo" style="margin-top: 10px; font-size: 13px;"><strong>Now Available!</strong> <a href="/index.php?app-download=blackberry">Download WordPress for BlackBerry</a></span><br /><br />' );
+ }
+ </script>
+ <?php
+}
+
+add_action( 'wp_mobile_theme_footer', 'jetpack_mobile_app_promo' );
+
+/**
+ * Adds an option to allow your Custom CSS to also be applied to the Mobile Theme.
+ * It's disabled by default, but this should allow people who know what they're
+ * doing to customize the mobile theme.
+ */
+function jetpack_mobile_css_settings() {
+ $mobile_css = get_option( 'wp_mobile_custom_css' );
+
+ ?>
+ <div class="misc-pub-section">
+ <label><?php esc_html_e( 'Mobile-compatible:', 'jetpack' ); ?></label>
+ <span id="mobile-css-display"><?php echo $mobile_css ? __( 'Yes', 'jetpack' ) : __( 'No', 'jetpack' ); ?></span>
+ <a class="edit-mobile-css hide-if-no-js" href="#mobile-css"><?php echo esc_html_e( 'Edit', 'jetpack' ); ?></a>
+ <div id="mobile-css-select" class="hide-if-js">
+ <input type="hidden" name="mobile_css" id="mobile-css" value="<?php echo intval( $mobile_css ); ?>" />
+ <label>
+ <input type="checkbox" id="mobile-css-visible" <?php checked( get_option( 'wp_mobile_custom_css' ) ); ?> />
+ <?php esc_html_e( 'Include this CSS in the Mobile Theme', 'jetpack' ); ?>
+ </label>
+ <p>
+ <a class="save-mobile-css hide-if-no-js button" href="#mobile-css"><?php esc_html_e( 'OK', 'jetpack' ); ?></a>
+ <a class="cancel-mobile-css hide-if-no-js" href="#mobile-css"><?php esc_html_e( 'Cancel', 'jetpack' ); ?></a>
+ </p>
+ </div>
+ </div>
+ <script type="text/javascript">
+ jQuery( function ( $ ) {
+ $( '.edit-mobile-css' ).bind( 'click', function ( e ) {
+ e.preventDefault();
+
+ $( '#mobile-css-select' ).slideDown();
+ $( this ).hide();
+ } );
+
+ $( '.cancel-mobile-css' ).bind( 'click', function ( e ) {
+ e.preventDefault();
+
+ $( '#mobile-css-select' ).slideUp( function () {
+ $( '.edit-mobile-css' ).show();
+
+ $( '#mobile-css-visible' ).prop( 'checked', $( '#mobile-css' ).val() == '1' );
+ } );
+ } );
+
+ $( '.save-mobile-css' ).bind( 'click', function ( e ) {
+ e.preventDefault();
+
+ $( '#mobile-css-select' ).slideUp();
+ $( '#mobile-css-display' ).text( $( '#mobile-css-visible' ).prop( 'checked' ) ? 'Yes' : 'No' );
+ $( '#mobile-css' ).val( $( '#mobile-css-visible' ).prop( 'checked' ) ? '1' : '0' );
+ $( '.edit-mobile-css' ).show();
+ } );
+ } );
+ </script>
+ <?php
+}
+
+add_action( 'custom_css_submitbox_misc_actions', 'jetpack_mobile_css_settings' );
+
+function jetpack_mobile_customizer_controls( $wp_customize ) {
+ $wp_customize->add_setting( 'wp_mobile_custom_css' , array(
+ 'default' => true,
+ 'transport' => 'postMessage',
+ 'type' => 'option'
+ ) );
+
+ $wp_customize->add_control( 'jetpack_mobile_css_control', array(
+ 'type' => 'checkbox',
+ 'label' => __( 'Include this CSS in the Mobile Theme', 'jetpack' ),
+ 'section' => 'jetpack_custom_css',
+ 'settings' => 'wp_mobile_custom_css',
+ ) );
+}
+
+add_action( 'jetpack_custom_css_customizer_controls', 'jetpack_mobile_customizer_controls' );
+
+function jetpack_mobile_save_css_settings() {
+ update_option( 'wp_mobile_custom_css', isset( $_POST['mobile_css'] ) && ! empty( $_POST['mobile_css'] ) );
+}
+
+add_action( 'safecss_save_pre', 'jetpack_mobile_save_css_settings' );
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/comments.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/comments.php
new file mode 100644
index 00000000..55c35161
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/comments.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * The template for displaying Comments.
+ *
+ * The area of the page that contains both current comments
+ * and the comment form. The actual display of comments is
+ * handled by a callback to minileven_comment() which is
+ * located in the functions.php file.
+ *
+ * @package Minileven
+ */
+?>
+ <div id="comments">
+ <?php if ( post_password_required() ) : ?>
+ <p class="nopassword"><?php _e( 'This post is password protected. Enter the password to view any comments.', 'jetpack' ); ?></p>
+ </div><!-- #comments -->
+ <?php
+ /* Stop the rest of comments.php from being processed,
+ * but don't kill the script entirely -- we still have
+ * to fully load the template.
+ */
+ return;
+ endif;
+ ?>
+
+ <?php // You can start editing here -- including this comment! ?>
+
+ <?php comment_form(); ?>
+
+ <?php if ( have_comments() ) : ?>
+ <ol class="commentlist">
+ <?php
+ /* Loop through and list the comments. Tell wp_list_comments()
+ * to use minileven_comment() to format the comments.
+ * If you want to overload this in a child theme then you can
+ * define minileven_comment() and that will be used instead.
+ * See minileven_comment() in minileven/functions.php for more.
+ */
+ wp_list_comments( array( 'callback' => 'minileven_comment' ) );
+ ?>
+ </ol>
+
+ <?php if ( get_comment_pages_count() > 1 && get_option( 'page_comments' ) ) : // are there comments to navigate through ?>
+ <nav id="comment-nav-below">
+ <h1 class="assistive-text"><?php _e( 'Comment navigation', 'jetpack' ); ?></h1>
+ <div class="nav-previous"><?php previous_comments_link( __( '&larr; Older Comments', 'jetpack' ) ); ?></div>
+ <div class="nav-next"><?php next_comments_link( __( 'Newer Comments &rarr;', 'jetpack' ) ); ?></div>
+ </nav>
+ <?php endif; // check for comment navigation
+ endif; // check for the existence of comments
+ ?>
+ </div><!-- #comments --> \ No newline at end of file
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/content-gallery.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/content-gallery.php
new file mode 100644
index 00000000..218949ea
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/content-gallery.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * The template for displaying posts in the Gallery Post Format on index and archive pages
+ *
+ * Learn more: http://codex.wordpress.org/Post_Formats
+ *
+ * @package Minileven
+ */
+?>
+
+<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
+ <header class="entry-header">
+ <div class="entry-heading">
+ <?php if ( '1' == get_option( 'wp_mobile_featured_images' ) && minileven_show_featured_images() ) : ?>
+ <div class="entry-thumbnail">
+ <a href="<?php the_permalink(); ?>" title="<?php echo esc_attr( sprintf( __( 'Permalink to %s', 'jetpack' ), the_title_attribute( 'echo=0' ) ) ); ?>" rel="<?php the_ID(); ?>" class="minileven-featured-thumbnail"><?php the_post_thumbnail(); ?></a>
+ </div><!-- .entry-thumbnail -->
+ <?php endif; ?>
+ <h2 class="entry-title"><a href="<?php the_permalink(); ?>" rel="bookmark"><?php the_title(); ?></a></h2>
+ <h3 class="entry-format"><?php _e( 'Gallery', 'jetpack' ); ?></h3>
+ </div>
+ </header><!-- .entry-header -->
+
+ <div class="entry-content">
+ <?php if ( is_single() || post_password_required() ) : ?>
+ <?php the_content( __( 'Continue reading <span class="meta-nav">&rarr;</span>', 'jetpack' ) ); ?>
+
+ <?php else : ?>
+ <?php
+ $images = minileven_get_gallery_images();
+ if ( $images ) :
+ $total_images = count( $images );
+ $large_image = array_shift( $images );
+ $thumb1_image = array_shift( $images );
+ $thumb2_image = array_shift( $images );
+ $thumb3_image = array_shift( $images );
+
+ $image_img_tag = wp_get_attachment_image( (int) $large_image, 'large' );
+ $thumb1_img_tag = wp_get_attachment_image( (int) $thumb1_image, 'thumbnail' );
+ $thumb2_img_tag = wp_get_attachment_image( (int) $thumb2_image, 'thumbnail' );
+ $thumb3_img_tag = wp_get_attachment_image( (int) $thumb3_image, 'thumbnail' );
+ ?>
+ <div class="img-gallery">
+ <div class="gallery-large">
+ <a href="<?php the_permalink(); ?>"><?php echo $image_img_tag; ?></a>
+ </div><!-- .gallery-large -->
+ <?php if ( 3 == $total_images ) : ?>
+ <div class="gallery-thumbs-2">
+ <a href="<?php the_permalink(); ?>" class="gallery-thumb-1"><?php echo $thumb1_img_tag; ?></a>
+ <a href="<?php the_permalink(); ?>" class="gallery-thumb-2"><?php echo $thumb2_img_tag; ?></a>
+ </div><!-- .gallery-thumbs -->
+
+ <?php elseif ( 4 <= $total_images ) : ?>
+ <div class="gallery-thumbs-3">
+ <a href="<?php the_permalink(); ?>" class="gallery-thumb-1"><?php echo $thumb1_img_tag; ?></a>
+ <a href="<?php the_permalink(); ?>" class="gallery-thumb-2"><?php echo $thumb2_img_tag; ?></a>
+ <a href="<?php the_permalink(); ?>" class="gallery-thumb-3"><?php echo $thumb3_img_tag; ?></a>
+ </div><!-- .gallery-thumbs -->
+ </div><!-- .img-gallery -->
+ <?php endif; ?>
+
+ <p class="gallery-info"><em><?php printf( _n( 'This gallery contains <a %1$s>%2$s photo</a>.', 'This gallery contains <a %1$s>%2$s photos</a>.', $total_images, 'jetpack' ),
+ 'href="' . esc_url( get_permalink() ) . '" title="' . esc_attr( sprintf( __( 'Permalink to %s', 'jetpack' ), the_title_attribute( 'echo=0' ) ) ) . '" rel="bookmark"',
+ number_format_i18n( $total_images ) );
+ ?></em></p>
+
+ <?php endif; ?>
+ <?php endif; ?>
+
+ <?php wp_link_pages( array( 'before' => '<div class="page-link"><span>' . __( 'Pages:', 'jetpack' ) . '</span>', 'after' => '</div>' ) ); ?>
+</div><!-- .entry-content -->
+
+ <footer class="entry-meta">
+ <?php minileven_posted_on(); ?>
+ <?php if ( comments_open() ) : ?>
+ <span class="comments-link"><?php comments_popup_link( '<span class="leave-reply">' . __( 'Leave a Reply', 'jetpack' ) . '</span>', __( '<b>1</b> Reply', 'jetpack' ), __( '<b>%</b> Replies', 'jetpack' ) ); ?></span>
+ <?php endif; // End if comments_open() ?>
+
+ <?php edit_post_link( __( 'Edit', 'jetpack' ), '<span class="edit-link">', '</span>' ); ?>
+ </footer><!-- #entry-meta -->
+</article><!-- #post-<?php the_ID(); ?> -->
+
+<?php comments_template( '', true ); ?>
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/content.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/content.php
new file mode 100644
index 00000000..e434ea41
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/content.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * The default template for displaying content
+ *
+ * @package Minileven
+ */
+?>
+
+ <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
+ <header class="entry-header">
+ <?php if ( '1' == get_option( 'wp_mobile_featured_images' ) && minileven_show_featured_images() ) : ?>
+ <div class="entry-thumbnail">
+ <a href="<?php the_permalink(); ?>" title="<?php echo esc_attr( sprintf( __( 'Permalink to %s', 'jetpack' ), the_title_attribute( 'echo=0' ) ) ); ?>" rel="<?php the_ID(); ?>" class="minileven-featured-thumbnail"><?php the_post_thumbnail(); ?></a>
+ </div><!-- .entry-thumbnail -->
+ <?php endif; ?>
+ <?php if ( is_sticky() ) : ?>
+ <div class="entry-heading">
+ <h2 class="entry-title"><a href="<?php the_permalink(); ?>" rel="bookmark"><?php the_title(); ?></a></h2>
+ <h3 class="entry-format"><?php _e( 'Featured', 'jetpack' ); ?></h3>
+ <div>
+ <?php else : ?>
+ <h1 class="entry-title"><a href="<?php the_permalink(); ?>" rel="bookmark"><?php the_title(); ?></a></h1>
+ <?php endif; ?>
+
+ <div class="entry-meta">
+ <?php if ( is_singular() && is_multi_author() ) : ?>
+ <span class="author-link">
+ <?php _e( 'Posted by ', 'jetpack' ); ?>
+ <?php the_author_posts_link(); ?>
+ </span><!-- .author-link -->
+ <?php endif; ?>
+ </div><!-- .entry-meta -->
+ </header><!-- .entry-header -->
+
+ <div class="entry-content">
+ <?php if ( '1' == get_option( 'wp_mobile_excerpt' ) && ( is_home() || is_search() || is_archive() ) ) : ?>
+ <?php echo minileven_excerpt( 300 ); ?>
+ <?php else : ?>
+ <?php the_content( __( 'Continue reading <span class="meta-nav">&rarr;</span>', 'jetpack' ) ); ?>
+ <?php endif; ?>
+ <?php wp_link_pages( array( 'before' => '<div class="page-link"><span>' . __( 'Pages:', 'jetpack' ) . '</span>', 'after' => '</div>' ) ); ?>
+ </div><!-- .entry-content -->
+
+ <footer class="entry-meta">
+ <?php if ( 'post' == get_post_type() ) : ?>
+ <?php minileven_posted_on(); ?>
+ <?php endif; ?>
+ <?php if ( comments_open() ) : ?>
+ <span class="comments-link"><?php comments_popup_link( '<span class="leave-reply">' . __( 'Leave a reply', 'jetpack' ) . '</span>', __( '<b>1</b> Reply', 'jetpack' ), __( '<b>%</b> Replies', 'jetpack' ) ); ?></span>
+ <?php endif; // End if comments_open() ?>
+ <?php edit_post_link( __( 'Edit', 'jetpack' ), '<span class="edit-link">', '</span>' ); ?>
+ </footer><!-- #entry-meta -->
+ </article><!-- #post-<?php the_ID(); ?> -->
+
+ <?php if ( is_single() ) : ?>
+ <nav id="nav-single">
+ <h3 class="assistive-text"><?php _e( 'Post navigation', 'jetpack' ); ?></h3>
+ <span class="nav-previous"><?php previous_post_link( '%link', __( '&laquo; Previous', 'jetpack' ) ); ?></span>
+ <span class="nav-next"><?php next_post_link( '%link', __( 'Next &raquo;', 'jetpack' ) ); ?></span>
+ </nav><!-- #nav-single -->
+ <?php endif; ?>
+
+ <?php comments_template( '', true ); ?>
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/footer.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/footer.php
new file mode 100644
index 00000000..964165f0
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/footer.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * The template for displaying the footer.
+ *
+ * Contains the closing of the id=main div and all content after
+ *
+ * @package Minileven
+ */
+?>
+
+ </div><!-- #main -->
+</div><!-- #page -->
+<?php get_sidebar(); ?>
+
+</div><!-- #wrapper -->
+
+<?php
+ /**
+ * Fires before the Mobile Theme's <footer> tag.
+ *
+ * @module minileven
+ *
+ * @since 3.7.0
+ */
+ do_action( 'jetpack_mobile_footer_before' );
+?>
+
+<footer id="colophon" role="contentinfo">
+ <div id="site-generator">
+ <a href="<?php echo esc_url( home_url( add_query_arg('ak_action', 'reject_mobile') ) ); ?>"><?php _e( 'View Full Site', 'jetpack' ); ?></a><br />
+
+ <?php
+ /**
+ * Fires after the View Full Site link in the Mobile Theme's footer.
+ *
+ * By default, a promo to download the native apps is added to this action.
+ *
+ * @module minileven
+ *
+ * @since 1.8.0
+ */
+ do_action( 'wp_mobile_theme_footer' );
+
+ /**
+ * Fires before the credit links in the Mobile Theme's footer.
+ *
+ * @module minilven
+ *
+ * @since 1.8.0
+ */
+ do_action( 'minileven_credits' );
+ ?>
+
+ <a href="<?php echo esc_url( __( 'https://wordpress.org/', 'jetpack' ) ); ?>" rel="noopener noreferrer" target="_blank" title="<?php esc_attr_e( 'Semantic Personal Publishing Platform', 'jetpack' ); ?>" rel="generator"><?php printf( __( 'Proudly powered by %s', 'jetpack' ), 'WordPress' ); ?></a>
+ </div>
+</footer><!-- #colophon -->
+
+<?php wp_footer(); ?>
+
+</body>
+</html>
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/functions.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/functions.php
new file mode 100644
index 00000000..fadd678c
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/functions.php
@@ -0,0 +1,273 @@
+<?php
+/**
+ * Minileven functions and definitions
+ *
+ * Sets up the theme and provides some helper functions. Some helper functions
+ * are used in the theme as custom template tags. Others are attached to action and
+ * filter hooks in WordPress to change core functionality.
+ *
+ * The first function, minileven_setup(), sets up the theme by registering support
+ * for various features in WordPress, such as post thumbnails, navigation menus, and the like.
+ *
+ * @package Minileven
+ */
+
+/**
+ * Set the content width based on the theme's design and stylesheet.
+ */
+if ( ! isset( $content_width ) )
+ $content_width = 584;
+
+/**
+ * Tell WordPress to run minileven_setup() when the 'after_setup_theme' hook is run.
+ */
+add_action( 'after_setup_theme', 'minileven_setup' );
+
+if ( ! function_exists( 'minileven_setup' ) ):
+/**
+ * Sets up theme defaults and registers support for various WordPress features.
+ */
+function minileven_setup() {
+ global $wp_version;
+
+ /**
+ * Custom template tags for this theme.
+ */
+ require( get_template_directory() . '/inc/template-tags.php' );
+
+ /**
+ * Custom functions that act independently of the theme templates
+ */
+ require( get_template_directory() . '/inc/tweaks.php' );
+
+ /**
+ * Implement the Custom Header functions
+ */
+ require( get_template_directory() . '/inc/custom-header.php' );
+
+ /* Make Minileven available for translation.
+ * Translations can be added to the /languages/ directory.
+ * If you're building a theme based on Minileven, use a find and replace
+ * to change 'minileven' to the name of your theme in all the template files.
+ */
+/* Don't load a minileven textdomain, as it uses the Jetpack textdomain.
+ load_theme_textdomain( 'minileven', get_template_directory() . '/languages' );
+*/
+
+ // Add default posts and comments RSS feed links to <head>.
+ add_theme_support( 'automatic-feed-links' );
+
+ // This theme uses wp_nav_menu() in one location.
+ register_nav_menu( 'primary', __( 'Primary Menu', 'jetpack' ) );
+
+ // Add support for a variety of post formats
+ add_theme_support( 'post-formats', array( 'gallery' ) );
+
+ // Add support for custom backgrounds
+ add_theme_support( 'custom-background' );
+
+ // Add support for post thumbnails
+ add_theme_support( 'post-thumbnails' );
+}
+endif; // minileven_setup
+
+/**
+ * Enqueue scripts and styles
+ */
+function minileven_scripts() {
+ global $post;
+
+ wp_enqueue_style( 'style', get_stylesheet_uri() );
+
+ wp_enqueue_script(
+ 'small-menu',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/minileven/theme/pub/minileven/js/small-menu.min.js',
+ 'modules/minileven/theme/pub/minileven/js/small-menu.js'
+ ),
+ array( 'jquery' ),
+ '20120206',
+ true
+ );
+
+ if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
+ wp_enqueue_script( 'comment-reply' );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'minileven_scripts' );
+
+function minileven_fonts() {
+
+ /* translators: If there are characters in your language that are not supported
+ by Open Sans, translate this to 'off'. Do not translate into your own language. */
+
+ if ( 'off' !== _x( 'on', 'Open Sans font: on or off', 'jetpack' ) ) {
+
+ $opensans_subsets = 'latin,latin-ext';
+
+ /* translators: To add an additional Open Sans character subset specific to your language, translate
+ this to 'greek', 'cyrillic' or 'vietnamese'. Do not translate into your own language. */
+ $opensans_subset = _x( 'no-subset', 'Open Sans font: add new subset (greek, cyrillic, vietnamese)', 'jetpack' );
+
+ if ( 'cyrillic' == $opensans_subset )
+ $opensans_subsets .= ',cyrillic,cyrillic-ext';
+ elseif ( 'greek' == $opensans_subset )
+ $opensans_subsets .= ',greek,greek-ext';
+ elseif ( 'vietnamese' == $opensans_subset )
+ $opensans_subsets .= ',vietnamese';
+
+ $opensans_query_args = array(
+ 'family' => 'Open+Sans:200,200italic,300,300italic,400,400italic,600,600italic,700,700italic',
+ 'subset' => $opensans_subsets,
+ );
+ wp_register_style( 'minileven-open-sans', add_query_arg( $opensans_query_args, "//fonts.googleapis.com/css" ), array(), null );
+ }
+}
+add_action( 'init', 'minileven_fonts' );
+
+/**
+ * Register our sidebars and widgetized areas.
+ * @since Minileven 1.0
+ */
+function minileven_widgets_init() {
+ register_sidebar( array(
+ 'name' => __( 'Main Sidebar', 'jetpack' ),
+ 'id' => 'sidebar-1',
+ 'before_widget' => '<aside id="%1$s" class="widget %2$s">',
+ 'after_widget' => "</aside>",
+ 'before_title' => '<h3 class="widget-title">',
+ 'after_title' => '</h3>',
+ ) );
+}
+add_action( 'widgets_init', 'minileven_widgets_init' );
+
+function minileven_posts_per_page() {
+ return 5;
+}
+add_filter('pre_option_posts_per_page', 'minileven_posts_per_page');
+
+/**
+ * Determine the currently active theme.
+ */
+function minileven_actual_current_theme() {
+ $removed = remove_action( 'option_stylesheet', 'jetpack_mobile_stylesheet' );
+ $stylesheet = get_option( 'stylesheet' );
+ if ( $removed )
+ add_action( 'option_stylesheet', 'jetpack_mobile_stylesheet' );
+
+ return $stylesheet;
+}
+
+/* This function grabs the location of the custom menus from the current theme. If no menu is set in a location
+* it will return a boolean "false". This function helps Minileven know which custom menu to display. */
+function minileven_get_menu_location() {
+ $theme_slug = minileven_actual_current_theme();
+ $mods = get_option( "theme_mods_{$theme_slug}" );
+
+ if ( has_filter( 'jetpack_mobile_theme_menu' ) ) {
+
+ /**
+ * Filter the menu displayed in the Mobile Theme.
+ *
+ * @module minileven
+ *
+ * @since 3.4.0
+ *
+ * @param int $menu_id ID of the menu to display.
+ */
+ return array( 'primary' => apply_filters( 'jetpack_mobile_theme_menu', $menu_id ) );
+ }
+
+ if ( isset( $mods['nav_menu_locations'] ) && ! empty( $mods['nav_menu_locations'] ) )
+ return $mods['nav_menu_locations'];
+
+ return false;
+}
+
+/* This function grabs the custom background image from the user's current theme so that Minileven can display it. */
+function minileven_get_background() {
+ $theme_slug = minileven_actual_current_theme();
+ $mods = get_option( "theme_mods_$theme_slug" );
+
+ if ( ! empty( $mods ) ) {
+ return array(
+ 'color' => isset( $mods['background_color'] ) ? $mods['background_color'] : null,
+ 'image' => isset( $mods['background_image'] ) ? $mods['background_image'] : null,
+ 'repeat' => isset( $mods['background_repeat'] ) ? $mods['background_repeat'] : null,
+ 'position' => isset( $mods['background_position_x'] ) ? $mods['background_position_x'] : null,
+ 'attachment' => isset( $mods['attachment'] ) ? $mods['attachment'] : null,
+ );
+ }
+ return false;
+}
+
+/**
+ * If the user has set a static front page, show all posts on the front page, instead of a static page.
+ */
+if ( '1' == get_option( 'wp_mobile_static_front_page' ) )
+ add_filter( 'pre_option_page_on_front', '__return_zero' );
+
+/**
+ * Retrieves the IDs for images in a gallery.
+ *
+ * @uses get_post_galleries() first, if available. Falls back to shortcode parsing,
+ * then as last option uses a get_posts() call.
+ *
+ * @return array List of image IDs from the post gallery.
+ */
+function minileven_get_gallery_images() {
+ $images = array();
+
+ if ( function_exists( 'get_post_galleries' ) ) {
+ $galleries = get_post_galleries( get_the_ID(), false );
+ if ( isset( $galleries[0]['ids'] ) )
+ $images = explode( ',', $galleries[0]['ids'] );
+ } else {
+ $pattern = get_shortcode_regex();
+ preg_match( "/$pattern/s", get_the_content(), $match );
+ $atts = shortcode_parse_atts( $match[3] );
+ if ( isset( $atts['ids'] ) )
+ $images = explode( ',', $atts['ids'] );
+ }
+
+ if ( ! $images ) {
+ $images = get_posts( array(
+ 'fields' => 'ids',
+ 'numberposts' => 999,
+ 'order' => 'ASC',
+ 'orderby' => 'menu_order',
+ 'post_mime_type' => 'image',
+ 'post_parent' => get_the_ID(),
+ 'post_type' => 'attachment',
+ 'suppress_filters' => false,
+ ) );
+ }
+
+ return $images;
+}
+
+/**
+ * Allow plugins to filter where Featured Images are displayed.
+ * Default has Featured Images disabled on single view and pages.
+ *
+ * @uses is_search()
+ * @uses apply_filters()
+ * @return bool
+ */
+function minileven_show_featured_images() {
+ $enabled = ( is_home() || is_search() || is_archive() ) ? true : false;
+
+ /**
+ * Filter where featured images are displayed in the Mobile Theme.
+ *
+ * By setting $enabled to true or false using functions like is_home() or
+ * is_archive(), you can control where featured images are be displayed.
+ *
+ * @module minileven
+ *
+ * @since 3.2.0
+ *
+ * @param bool $enabled True if featured images should be displayed, false if not.
+ */
+ return (bool) apply_filters( 'minileven_show_featured_images', $enabled );
+}
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/header.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/header.php
new file mode 100644
index 00000000..2488a47f
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/header.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * The Header for our theme.
+ *
+ * Displays all of the <head> section and everything up till <div id="main">
+ *
+ * @package Minileven
+ */
+?><!DOCTYPE html>
+<html <?php language_attributes(); ?>>
+<head>
+<meta charset="<?php bloginfo( 'charset' ); ?>" />
+<meta name="viewport" content="width=device-width" />
+<title><?php wp_title( '|', true, 'right' ); ?></title>
+<link rel="profile" href="http://gmpg.org/xfn/11" />
+<link rel="pingback" href="<?php bloginfo( 'pingback_url' ); ?>" />
+<?php wp_head(); ?>
+</head>
+
+<body <?php body_class(); ?>>
+<div id="wrapper">
+ <?php
+ $location = minileven_get_menu_location(); // get the menu locations from the current theme in use
+ ?>
+ <div class="menu-search">
+ <nav id="access" class="site-navigation main-navigation" role="navigation">
+ <h3 class="menu-toggle"><?php _e( 'Menu', 'jetpack' ); ?></h3>
+
+ <?php /* Allow screen readers / text browsers to skip the navigation menu and get right to the good stuff. */ ?>
+ <div class="skip-link"><a class="assistive-text" href="#content"><?php _e( 'Skip to primary content', 'jetpack' ); ?></a></div>
+ <?php /* Our navigation menu. If one isn't filled out, wp_nav_menu falls back to wp_page_menu. The menu assiged to the primary position is the one used. If none is assigned, the menu with the lowest ID is used. */
+ if ( false !== $location ) :
+ $location_values = array_values( $location );
+ $menu_id = array_shift( $location_values ); // acccess the ID of the menu assigned to that location. Using only the first menu ID returned in the array.
+ wp_nav_menu( array( 'theme_location' => 'primary', 'container_class' => '', 'menu_class' => 'nav-menu', 'menu' => $menu_id ) );
+ else: // if the $location variable is false, wp_page_menu() is shown instead.
+ wp_nav_menu( array( 'theme_location' => 'primary', 'container_class' => '', 'menu_class' => 'nav-menu' ) );
+ endif;
+ ?>
+ </nav><!-- #access -->
+ <div class="search-form">
+ <?php get_search_form(); ?>
+ </div><!-- .search-form-->
+ </div><!-- .menu-search-->
+
+ <?php
+ /**
+ * Fires before Minileven header.
+ *
+ * @module minileven
+ *
+ * @since 3.4.0
+ */
+ do_action( 'jetpack_mobile_header_before' );
+
+ if ( function_exists( 'minileven_header' ) )
+ minileven_header();
+
+ /**
+ * Fires after Minileven header.
+ *
+ * @module minilven
+ *
+ * @since 3.4.0
+ */
+ do_action( 'jetpack_mobile_header_after' );
+ ?>
+
+ <div id="page" class="hfeed">
+ <div id="main">
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/image.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/image.php
new file mode 100644
index 00000000..02032c2c
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/image.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * The template for displaying image attachments.
+ *
+ * @package Minileven
+ */
+
+get_header(); ?>
+
+ <div id="primary" class="image-attachment">
+ <div id="content" role="main">
+
+ <?php while ( have_posts() ) : the_post(); ?>
+
+ <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
+ <header class="entry-header">
+ <h1 class="entry-title"><?php the_title(); ?></h1>
+ </header><!-- .entry-header -->
+
+ <div class="entry-content">
+
+ <div class="entry-attachment">
+ <div class="attachment">
+<?php
+ /**
+ * Grab the IDs of all the image attachments in a gallery so we can get the URL of the next adjacent image in a gallery,
+ * or the first image (if we're looking at the last image in a gallery), or, in a gallery of one, just the link to that image file
+ */
+ $attachments = array_values( get_children( array( 'post_parent' => $post->post_parent, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => 'ASC', 'orderby' => 'menu_order ID', 'suppress_filters' => false ) ) );
+ foreach ( $attachments as $k => $attachment ) {
+ if ( $attachment->ID == $post->ID )
+ break;
+ }
+ $k++;
+ // If there is more than 1 attachment in a gallery
+ if ( count( $attachments ) > 1 ) {
+ if ( isset( $attachments[ $k ] ) )
+ // get the URL of the next image attachment
+ $next_attachment_url = get_attachment_link( $attachments[ $k ]->ID );
+ else
+ // or get the URL of the first image attachment
+ $next_attachment_url = get_attachment_link( $attachments[ 0 ]->ID );
+ } else {
+ // or, if there's only 1 image, get the URL of the image
+ $next_attachment_url = wp_get_attachment_url();
+ }
+?>
+ <a href="<?php echo esc_url( $next_attachment_url ); ?>" title="<?php echo esc_attr( get_the_title() ); ?>" rel="attachment"><?php
+
+ /**
+ * Filter the Mobile Theme image size.
+ *
+ * @module minileven
+ *
+ * @since 1.8.0
+ *
+ * @param int Image size in pixels.
+ */
+ $attachment_size = apply_filters( 'minileven_attachment_size', 848 );
+ echo wp_get_attachment_image( $post->ID, array( $attachment_size, 1024 ) ); // filterable image width with 1024px limit for image height.
+ ?></a>
+
+ <?php if ( ! empty( $post->post_excerpt ) ) : ?>
+ <div class="entry-caption">
+ <?php the_excerpt(); ?>
+ </div>
+ <?php endif; ?>
+ </div><!-- .attachment -->
+
+ </div><!-- .entry-attachment -->
+
+ <div class="entry-description">
+ <?php the_content(); ?>
+ <?php wp_link_pages( array( 'before' => '<div class="page-link"><span>' . __( 'Pages:', 'jetpack' ) . '</span>', 'after' => '</div>' ) ); ?>
+ </div><!-- .entry-description -->
+
+ </div><!-- .entry-content -->
+
+ <footer class="entry-meta">
+ <div class="attachment-meta">
+ <?php
+ $metadata = wp_get_attachment_metadata();
+ printf( __( '<span class="entry-gallery">&laquo; <a href="%1$s" title="Back to %2$s" rel="gallery">Back to Gallery</a></span>', 'jetpack' ),
+ esc_url( get_permalink( $post->post_parent ) ),
+ get_the_title( $post->post_parent )
+ );
+ ?>
+ </div><!-- .attachment-meta-->
+ <?php if ( comments_open() ) : ?>
+ <span class="comments-link"><?php comments_popup_link( '<span class="leave-reply">' . __( 'Leave a reply', 'jetpack' ) . '</span>', __( '<b>1</b> Reply', 'jetpack' ), __( '<b>%</b> Replies', 'jetpack' ) ); ?></span>
+ <?php endif; // End if comments_open() ?>
+ <?php edit_post_link( __( 'Edit', 'jetpack' ), '<span class="edit-link">', '</span>' ); ?>
+ </footer><!-- #entry-meta -->
+ </article><!-- #post-<?php the_ID(); ?> -->
+
+ <nav id="nav-single">
+ <h3 class="assistive-text"><?php _ex( 'Image navigation', 'next-saturday' , 'jetpack' ); ?></h3>
+ <span class="nav-previous"><?php previous_image_link( false, __( '&laquo; Previous' , 'jetpack' ) ); ?></span>
+ <span class="nav-next"><?php next_image_link( false, __( 'Next &raquo; ' , 'jetpack' ) ); ?></span>
+ </nav><!-- #nav-single -->
+
+ <?php comments_template(); ?>
+
+ <?php endwhile; // end of the loop. ?>
+
+ </div><!-- #content -->
+ </div><!-- #primary -->
+<?php get_footer(); ?>
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/custom-header.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/custom-header.php
new file mode 100644
index 00000000..dc8758b7
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/custom-header.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ * @package Minileven
+ * @since Minileven 2.0
+ */
+
+/* This function grabs the custom header from the current theme so that Minileven can display it. */
+function minileven_get_header_image() {
+ $theme_slug = minileven_actual_current_theme();
+ $mods = get_option( "theme_mods_{$theme_slug}" );
+
+ if ( isset( $mods['header_image'] ) && 'remove-header' != $mods['header_image'] && 'random-default-image' != $mods['header_image'] && 'random-uploaded-image' != $mods['header_image'] )
+ return $mods['header_image'];
+
+ return false;
+}
+
+/* This function determines whether or not the user is displaying the header on the current theme */
+function minileven_header_text_display() {
+ $theme_slug = minileven_actual_current_theme();
+ $mods = get_option( "theme_mods_{$theme_slug}" );
+
+ if ( isset( $mods['header_textcolor'] ) )
+ return $mods['header_textcolor'];
+
+ return false;
+}
+
+/* This function determines how the header should be displayed. */
+function minileven_header() {
+ $header_image = minileven_get_header_image();
+ $header_text = minileven_header_text_display();
+
+ if ( 'blank' != $header_text || false != $header_image ) : ?>
+
+ <header id="branding" role="banner">
+ <?php if ( 'blank' != $header_text ) : ?>
+ <div class="site-branding">
+ <h1 id="site-title"><span><a href="<?php echo esc_url( home_url( '/' ) ); ?>" title="<?php echo esc_attr( get_bloginfo( 'name', 'display' ) ); ?>" rel="home"><?php bloginfo( 'name' ); ?></a></span></h1>
+ <h2 id="site-description"><?php bloginfo( 'description' ); ?></h2>
+ </div>
+ <?php endif;
+
+ if ( false !== $header_image ) : ?>
+ <div id="header-img">
+ <a href="<?php echo esc_url( home_url( '/' ) ); ?>">
+ <img src="<?php echo $header_image; ?>" alt="" />
+ </a>
+ </div><!-- #header-img -->
+ <?php endif; // end check for header image existence. ?>
+ </header><!-- #branding -->
+<?php endif; // end check for both header text and header image
+}
+
+/* This function displays the custom background image or color, and custom text color */
+function minileven_show_background_and_header_color() {
+ $background = minileven_get_background();
+ $header_text = minileven_header_text_display();
+
+ $style = '';
+
+ if ( $background['color'] || $background['image'] ) :
+ $style = $background['color'] ? "background-color: #$background[color];" : '';
+
+ if ( $background['image'] ) :
+ $image = " background-image: url('$background[image]');";
+
+ if ( ! in_array( $background['repeat'], array( 'no-repeat', 'repeat-x', 'repeat-y', 'repeat' ) ) )
+ $background['repeat'] = 'repeat';
+ $repeat = " background-repeat: $background[repeat];";
+
+ if ( ! in_array( $background['position'], array( 'center', 'right', 'left' ) ) )
+ $background['position'] = 'left';
+ $position = " background-position: top $background[position];";
+
+ if ( ! in_array( $background['attachment'], array( 'fixed', 'scroll' ) ) )
+ $background['attachment'] = 'scroll';
+ $attachment = " background-attachment: $background[attachment];";
+
+ $style .= $image . $repeat . $position . $attachment;
+ endif;
+ endif;
+?>
+ <style type="text/css">
+ <?php if ( $style ) { ?>
+ body {
+ <?php echo trim( $style ); ?>
+ }
+ <?php } ?>
+ #page,
+ #branding {
+ margin: 0.6em 0.6em 0.8em;
+ }
+ #site-generator {
+ border: 0;
+ }
+ <?php if ( 'blank' != $header_text && '1' != get_option( 'wp_mobile_header_color' ) ) : ?>
+ /* If The user has set a header text color, use that */
+ #site-title,
+ #site-title a {
+ color: #<?php echo $header_text; ?>;
+ <?php endif; ?>
+ }
+ </style>
+<?php
+}
+add_action( 'wp_head', 'minileven_show_background_and_header_color' ); \ No newline at end of file
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.eot b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.eot
new file mode 100644
index 00000000..5a60506a
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.eot
Binary files differ
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.svg b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.svg
new file mode 100644
index 00000000..3dce209e
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="genericonsregular" horiz-adv-x="2048" >
+<font-face units-per-em="2048" ascent="1638" descent="-410" />
+<missing-glyph horiz-adv-x="500" />
+<glyph unicode="&#x2000;" horiz-adv-x="1024" />
+<glyph unicode="&#x2001;" />
+<glyph unicode="&#x2002;" horiz-adv-x="1024" />
+<glyph unicode="&#x2003;" />
+<glyph unicode="&#x2004;" horiz-adv-x="682" />
+<glyph unicode="&#x2005;" horiz-adv-x="512" />
+<glyph unicode="&#x2006;" horiz-adv-x="341" />
+<glyph unicode="&#x2007;" horiz-adv-x="341" />
+<glyph unicode="&#x2008;" horiz-adv-x="256" />
+<glyph unicode="&#x2009;" horiz-adv-x="409" />
+<glyph unicode="&#x200a;" horiz-adv-x="113" />
+<glyph unicode="&#x202f;" horiz-adv-x="409" />
+<glyph unicode="&#x205f;" horiz-adv-x="512" />
+<glyph unicode="&#xe000;" horiz-adv-x="500" d="M0 0z" />
+<glyph unicode="&#xf100;" d="M512 512v128h768v-128h-768zM512 768v128h256v-128h-256zM512 1024v128h640v-128h-640zM512 1280v128h1024v-128h-1024zM896 768v128h640v-128h-640zM1280 1024v128h256v-128h-256z" />
+<glyph unicode="&#xf101;" d="M256 1024q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5zM768 1024q0 -106 75 -181t181 -75t181 75t75 181t-75 181t-181 75t-181 -75t-75 -181z" />
+<glyph unicode="&#xf102;" d="M128 384v896l512 128l128 256h512l128 -256h512v-1024h-1792zM256 1440v160h256v-96zM576 960q0 -185 131.5 -316.5t316.5 -131.5q186 0 317 131.5t131 316.5t-131 316.5t-317 131.5q-185 0 -316.5 -131.5t-131.5 -316.5zM704 960q0 133 93.5 226.5t226.5 93.5 t226.5 -93.5t93.5 -226.5t-93.5 -226.5t-226.5 -93.5t-226.5 93.5t-93.5 226.5z" />
+<glyph unicode="&#xf103;" d="M128 512v384h384v-384h-384zM128 1024v384h896v-384h-896zM640 512v384h384v-384h-384zM1152 512v896h896v-896h-896z" />
+<glyph unicode="&#xf104;" d="M512 384v1280l1152 -640z" />
+<glyph unicode="&#xf105;" d="M640 1408q0 159 112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5q0 -124 -71.5 -222t-184.5 -138v-536l-256 -128v664q-113 40 -184.5 138t-71.5 222z" />
+<glyph unicode="&#xf106;" d="M384 640l128 768h512l-256 -768h-384zM1152 640l128 768h512l-256 -768h-384z" />
+<glyph unicode="&#xf107;" d="M376 852q0 108 72 204l160 128l96 -96l-160 -128q-48 -96 0 -192l96 -96q96 -48 192 0l128 160l96 -96l-128 -160q-96 -72 -204 -72t-180 72l-96 96q-72 72 -72 180zM736 960l352 352l96 -96l-352 -352zM864 1440l128 160q96 72 204 72t180 -72l96 -96q72 -72 72 -180 t-72 -204l-160 -128l-96 96l160 128q48 96 0 192l-96 96q-96 48 -192 0l-128 -160z" />
+<glyph unicode="&#xf108;" d="M0 1152v384q0 96 80 176t176 80h1024q96 0 176 -80t80 -176v-384q0 -96 -80 -176t-176 -80h-448l-448 -448v448h-128q-96 0 -176 80t-80 176zM768 640l128 128h384q168 0 276 108t108 276v384q96 0 176 -80t80 -176v-384q0 -96 -80 -176t-176 -80h-128v-448l-448 448 h-320z" />
+<glyph unicode="&#xf109;" d="M256 768v512h384l384 384v-1280l-384 384h-384zM1216 832q21 43 32 66.5t21.5 59.5t10.5 66t-10.5 66t-21.5 59.5t-32 66.5l96 96q96 -144 96 -288q0 -160 -96 -256zM1376 672q74 74 117 166t43 186t-43 186t-117 166l96 96q95 -71 143.5 -186.5t48.5 -261.5 t-48.5 -261.5t-143.5 -186.5z" />
+<glyph unicode="&#xf200;" d="M0 1024q0 212 79.5 402t216 326.5t326.5 216t402 79.5t402 -79.5t326.5 -216t216 -326.5t79.5 -402t-79.5 -402t-216 -326.5t-326.5 -216t-402 -79.5t-402 79.5t-326.5 216t-216 326.5t-79.5 402zM128 1024q0 -307 173.5 -536.5t466.5 -327.5v224q0 224 128 224 q-125 0 -213.5 16t-146.5 47t-91.5 83t-47 115.5t-13.5 154.5q0 100 25 187t71 133q-28 71 -28 143t14 109l14 36q7 0 18 -1t44 -7.5t63 -18.5t67 -38.5t64 -62.5q144 24 300 24t276 -24q27 36 60 62.5t60.5 38.5t51.5 18.5t38 7.5l14 1q4 -5 10 -14.5t19 -40.5t20 -63 t4 -78.5t-21 -91.5q96 -144 96 -320q0 -113 -21 -187.5t-75 -128t-147.5 -77t-236.5 -23.5q49 -25 72.5 -77.5t23.5 -146.5v-224q293 98 466.5 327.5t173.5 536.5q0 176 -73 341.5t-194.5 287t-287 194.5t-341.5 73t-341.5 -73t-287 -194.5t-194.5 -287t-73 -341.5z" />
+<glyph unicode="&#xf201;" d="M0 1024q0 206 82 395.5t219.5 327t327 219.5t395.5 82t395.5 -82t327 -219.5t219.5 -327t82 -395.5t-82 -395.5t-219.5 -327t-327 -219.5t-395.5 -82t-395.5 82t-327 219.5t-219.5 327t-82 395.5zM128 1024q0 -167 58 -319.5t166 -272.5q125 205 339 360t445 232 q-16 48 -80 176q-282 -86 -481.5 -111t-446.5 -1v-64zM160 1232q194 -22 444 14t388 82q-141 282 -320 528q-194 -85 -329.5 -247.5t-182.5 -376.5zM480 320q216 -192 544 -192q181 0 368 80q-33 300 -208 688q-222 -74 -410 -225.5t-294 -350.5zM832 1904 q102 -166 304 -512q6 2 86 31t118.5 45t108 47t122 64t93.5 69q-126 126 -290.5 199t-349.5 73q-32 0 -96 -8t-96 -8zM1200 1248q22 -29 36.5 -54.5t34 -67.5t25.5 -54q170 33 336 30t288 -30q-26 285 -160 464q-71 -57 -162 -104.5t-214.5 -100.5t-183.5 -83zM1344 928 q14 -27 43 -103t74.5 -231t74.5 -306q156 108 258 278t126 362q-276 46 -576 0z" />
+<glyph unicode="&#xf202;" d="M0 381q50 -6 100 -6q293 0 522 180q-137 2 -244.5 83t-147.5 208q44 -7 79 -7q57 0 110 15q-145 29 -241 144.5t-96 267.5v5q86 -48 191 -53q-86 58 -136.5 150t-50.5 200q0 113 57 211q158 -194 383 -310t483 -129q-11 49 -11 96q0 174 123 297t297 123q89 0 168.5 -35 t138.5 -97q142 27 266 102q-47 -150 -184 -233q124 15 241 66q-84 -127 -210 -217q2 -36 2 -55q0 -168 -49 -337t-150 -323.5t-241 -273.5t-336 -190t-420 -71q-351 0 -644 189z" />
+<glyph unicode="&#xf203;" d="M0 117q0 -48 34.5 -82.5t82.5 -34.5h1814q48 0 82.5 34.5t34.5 82.5v1814q0 48 -34.5 82.5t-82.5 34.5h-1814q-48 0 -82.5 -34.5t-34.5 -82.5v-1814zM900 969v303h222v258q0 78 26 147t77 124t136.5 87t194.5 32q55 0 108 -3t79 -6l26 -3l-7 -282h-193q-76 0 -101.5 -32 t-25.5 -101v-3v-2v-9v-207h329l-14 -303h-315v-841h-320v841h-222z" />
+<glyph unicode="&#xf204;" d="M640 969v303h222v258q0 78 26 147t77 124t136.5 87t194.5 32q55 0 108 -3t79 -6l26 -3l-7 -282h-193q-76 0 -101.5 -32t-25.5 -101v-3v-2v-9v-207h329l-14 -303h-315v-841h-320v841h-222z" />
+<glyph unicode="&#xf205;" d="M0 1024q0 208 81 397.5t218.5 327t327 218.5t397.5 81t398 -81t327 -218.5t218 -327t81 -397.5q0 -209 -81 -398.5t-218 -326.5t-326.5 -218t-398.5 -81q-208 0 -397.5 81t-327 218t-218.5 327t-81 398zM128 1024q0 -259 135.5 -473.5t359.5 -327.5l-421 1156 q-74 -168 -74 -355zM285 1530q4 0 13.5 -0.5t15.5 -0.5q70 0 127.5 2.5t80.5 4.5l22 3q29 2 46.5 -16t19.5 -40.5t-13 -43t-44 -24.5q-27 -4 -70 -8l295 -877l198 591l-104 283l-90 11q-38 2 -51.5 34t4.5 62t56 28q54 -10 201 -10q70 0 127.5 2.5t80.5 4.5l22 3 q38 2 56.5 -27t5 -60.5t-51.5 -36.5q-27 -4 -69 -8l293 -870l90 301q69 177 69 293q0 51 -14 107q-18 45 -40 81q-2 3 -15 24t-15.5 25t-13 22t-13 24.5t-10 22t-10 24.5t-6.5 22t-5 25t-1 24q0 63 42 110.5t106 49.5q-121 108 -274.5 168t-321.5 60q-226 0 -421 -105 t-318 -285zM772 165q123 -37 252 -37q152 0 296 51q0 1 -1 1l-1 2l-278 763zM1485 256q199 120 317 324t118 444q0 216 -99 409q4 -44 4 -68q0 -153 -69 -324z" />
+<glyph unicode="&#xf206;" d="M128 486v485q125 -127 330 -127q30 0 59 3q-32 -61 -32 -118q0 -33 13 -63t28.5 -48.5t45.5 -47.5q-18 0 -54.5 -0.5t-55.5 -0.5q-183 0 -334 -83zM128 1599v97q0 93 65.5 158.5t158.5 65.5h1344q93 0 158.5 -65.5t65.5 -158.5v-224h-280v280h-140v-280h-280v-140h280 v-280h140v280h280v-980q0 -93 -65.5 -158.5t-158.5 -65.5h-539q5 28 5 50q0 143 -46.5 230t-189.5 194q-3 2 -20.5 15t-25 19t-25.5 20t-27.5 22.5t-24 22t-23 23.5t-17 22t-12.5 22.5t-4 20.5q0 52 23 87t99 94q180 141 180 324q0 113 -45 204.5t-128 139.5h160l135 142 h-607q-127 0 -241.5 -49t-194.5 -132zM134 301q56 89 166.5 143.5t241.5 53.5q84 -1 158 -26q19 -13 62 -42.5t61 -42t48 -37t44.5 -41.5t29 -41.5t21.5 -49.5q7 -29 7 -66q0 -16 -1 -24h-620q-79 0 -140 49t-78 124zM228 1307q-21 161 50.5 269.5t194.5 104.5 q121 -4 215.5 -118.5t116.5 -277.5q21 -160 -43 -256t-187 -92q-125 4 -225.5 108t-121.5 262z" />
+<glyph unicode="&#xf207;" d="M384 1422q0 -58 40.5 -97.5t105.5 -39.5h1q67 0 108.5 39.5t41.5 97.5q-2 60 -42 98.5t-106 38.5q-67 0 -108 -39t-41 -98zM400 384h263v793h-263v-793zM809 384h264v443q0 45 8 64q16 40 50.5 68t85.5 28q133 0 133 -179v-424h264v455q0 175 -83.5 266t-220.5 91 q-50 0 -90.5 -12t-68.5 -34t-45 -41t-33 -44v112h-264v-793z" />
+<glyph unicode="&#xf208;" d="M128 384v1280q0 106 75 181t181 75h1280q106 0 181 -75t75 -181v-1280q0 -106 -75 -181t-181 -75h-1280q-106 0 -181 75t-75 181zM384 1422q0 -58 40.5 -97.5t105.5 -39.5h1q67 0 108.5 39.5t41.5 97.5q-2 60 -42 98.5t-106 38.5q-67 0 -108 -39t-41 -98zM400 384h263 v793h-263v-793zM809 384h264v443q0 45 8 64q16 40 50.5 68t85.5 28q133 0 133 -179v-424h264v455q0 175 -83.5 266t-220.5 91q-50 0 -90.5 -12t-68.5 -34t-45 -41t-33 -44v112h-264v-793z" />
+<glyph unicode="&#xf209;" d="M171 1260q0 109 35.5 219t110 213t179 182t254 126.5t323.5 47.5q176 0 327.5 -60.5t253.5 -161t160 -231t58 -270.5q0 -246 -85 -443t-241 -309.5t-355 -112.5q-99 0 -186.5 46.5t-121.5 110.5q-73 -290 -89 -347q-34 -123 -127 -270l-149 54q-7 167 22 290l162 688 q-40 81 -40 200q0 139 70.5 232.5t172.5 93.5q83 0 127 -53.5t44 -135.5q0 -51 -18.5 -124t-49 -170t-44.5 -154q-23 -99 37.5 -171t161.5 -72q117 0 209.5 92t142 244.5t49.5 334.5q0 214 -139 349t-387 135q-139 0 -257.5 -49.5t-197 -133t-122.5 -193t-44 -229.5 q0 -147 83 -247q18 -21 21.5 -34t-3.5 -37q-16 -61 -25 -101q-7 -24 -24.5 -32t-39.5 1q-127 51 -192.5 181.5t-65.5 300.5z" />
+<glyph unicode="&#xf210;" d="M0 1024q0 208 81 398t218.5 327t327 218t397.5 81q209 0 398.5 -81t326.5 -218t218 -326.5t81 -398.5t-81 -398.5t-218 -326.5t-326.5 -218t-398.5 -81q-147 0 -290 42q74 116 103 219l72 282q28 -53 99 -90.5t151 -37.5q162 0 288.5 91.5t195.5 251t69 359.5 q0 114 -47 220t-130 187.5t-206.5 130.5t-265.5 49q-141 0 -262 -38.5t-205.5 -103t-145.5 -147.5t-89.5 -172.5t-28.5 -178.5q0 -138 53 -243.5t156 -147.5q18 -8 32.5 -1t18.5 26q2 9 10 41t11 41q5 19 2.5 30t-16.5 28q-68 78 -68 200q0 97 35.5 186t99.5 156.5t160 108 t209 40.5q201 0 313.5 -109.5t112.5 -283.5q0 -148 -40 -271.5t-115 -198t-169 -74.5q-82 0 -131.5 58.5t-30.5 138.5q11 46 35.5 125t39.5 138t15 101q0 66 -35.5 109.5t-102.5 43.5q-82 0 -139.5 -76t-57.5 -189q0 -43 8 -83.5t16 -59.5l9 -19q-113 -475 -132 -558 q-24 -97 -18 -235q-275 120 -444 374t-169 564z" />
+<glyph unicode="&#xf211;" d="M160 1024q0 -172 122 -294t294 -122t294 122t122 294t-122 294t-294 122t-294 -122t-122 -294zM1056 1024q0 -172 122 -294t294 -122t294 122t122 294t-122 294t-294 122t-294 -122t-122 -294z" />
+<glyph unicode="&#xf300;" d="M256 896v384q0 106 75 181t181 75h1024q106 0 181 -75t75 -181v-384q0 -106 -75 -181t-181 -75h-448l-448 -448v448h-128q-106 0 -181 75t-75 181z" />
+<glyph unicode="&#xf301;" d="M384 512v1024h384l64 -128h448v-128h-640l-128 -256h128l64 128h960l-256 -640h-1024z" />
+<glyph unicode="&#xf302;" d="M256 768l768 768h512v-512l-768 -768zM1152 1280q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5t-37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5z" />
+<glyph unicode="&#xf303;" d="M256 1088q0 143 55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5t-55.5 -273.5t-150 -225t-225 -150t-273.5 -55.5t-273.5 55.5t-225 150t-150 225t-55.5 273.5zM384 1088q0 -117 45.5 -223.5t123 -184t184 -123t223.5 -45.5 t223.5 45.5t184 123t123 184t45.5 223.5t-45.5 223.5t-123 184t-184 123t-223.5 45.5t-223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5zM896 1062v474h128v-421l298 -298l-90 -91z" />
+<glyph unicode="&#xf304;" d="M512 384v256q0 159 112.5 271.5t271.5 112.5h256q159 0 271.5 -112.5t112.5 -271.5v-256h-1024zM768 1408q0 106 75 181t181 75t181 -75t75 -181t-75 -181t-181 -75t-181 75t-75 181z" />
+<glyph unicode="&#xf305;" d="M256 384v1280h256v128h128v-128h640v128h128v-128h256v-1280h-1408zM384 640q0 -53 37.5 -90.5t90.5 -37.5h896q53 0 90.5 37.5t37.5 90.5v640q0 53 -37.5 90.5t-90.5 37.5h-896q-53 0 -90.5 -37.5t-37.5 -90.5v-640zM768 1216q0 26 19 45t45 19h128q26 0 45 -19t19 -45 v-512q0 -26 -19 -45t-45 -19t-45 19t-19 45v448h-64q-26 0 -45 19t-19 45z" />
+<glyph unicode="&#xf306;" d="M256 384v1280h256v128h128v-128h640v128h128v-128h256v-1280h-1408zM384 640q0 -53 37.5 -90.5t90.5 -37.5h896q53 0 90.5 37.5t37.5 90.5v640q0 53 -37.5 90.5t-90.5 37.5h-896q-53 0 -90.5 -37.5t-37.5 -90.5v-640zM768 1216q0 26 19 45t45 19h256h2h1h3 q22 -2 38.5 -18t19.5 -39v-2v-2v-1v-2q0 -5 -2 -15l-128 -512q-6 -26 -28.5 -40t-48.5 -7q-26 6 -40 28.5t-7 48.5l108 433h-174q-26 0 -45 19t-19 45z" />
+<glyph unicode="&#xf307;" d="M256 384v1280h256v128h128v-128h640v128h128v-128h256v-1280h-1408zM384 640q0 -53 37.5 -90.5t90.5 -37.5h896q53 0 90.5 37.5t37.5 90.5v640q0 53 -37.5 90.5t-90.5 37.5h-896q-53 0 -90.5 -37.5t-37.5 -90.5v-640zM512 640v128h128v-128h-128zM512 896v128h128v-128 h-128zM768 640v128h128v-128h-128zM768 896v128h128v-128h-128zM768 1152v128h128v-128h-128zM1024 640v128h128v-128h-128zM1024 896v128h128v-128h-128zM1024 1152v128h128v-128h-128zM1280 896v128h128v-128h-128zM1280 1152v128h128v-128h-128z" />
+<glyph unicode="&#xf400;" d="M256 1216q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5q0 -184 -111 -337l495 -495l-128 -128l-495 495q-153 -111 -337 -111q-117 0 -223.5 45.5t-184 123t-123 184t-45.5 223.5zM384 1216q0 -185 131.5 -316.5 t316.5 -131.5q186 0 317 131.5t131 316.5t-131 316.5t-317 131.5q-185 0 -316.5 -131.5t-131.5 -316.5z" />
+<glyph unicode="&#xf401;" d="M256 1216q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5q0 -184 -111 -337l495 -495l-128 -128l-495 495q-153 -111 -337 -111q-117 0 -223.5 45.5t-184 123t-123 184t-45.5 223.5zM384 1216q0 -185 131.5 -316.5 t316.5 -131.5q186 0 317 131.5t131 316.5t-131 316.5t-317 131.5q-185 0 -316.5 -131.5t-131.5 -316.5zM512 1152v128h640v-128h-640z" />
+<glyph unicode="&#xf402;" d="M256 1216q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5q0 -184 -111 -337l495 -495l-128 -128l-495 495q-153 -111 -337 -111q-117 0 -223.5 45.5t-184 123t-123 184t-45.5 223.5zM384 1216q0 -185 131.5 -316.5 t316.5 -131.5q186 0 317 131.5t131 316.5t-131 316.5t-317 131.5q-185 0 -316.5 -131.5t-131.5 -316.5zM512 1152v128h256v256h128v-256h256v-128h-256v-256h-128v256h-256z" />
+<glyph unicode="&#xf403;" d="M0 1024l506 506q101 103 234.5 160.5t283.5 57.5t283.5 -57.5t233.5 -159.5l507 -507l-506 -507q-101 -103 -234.5 -160t-283.5 -57t-283.5 57.5t-233.5 160.5zM272 1024l370 -371q77 -78 175.5 -119.5t206.5 -41.5t206 41.5t174 118.5l373 372l-371 371 q-158 161 -382 161q-108 0 -206.5 -41t-173.5 -119zM640 1024q0 159 112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5zM1024 1152q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5t-37.5 90.5t-90.5 37.5 t-90.5 -37.5t-37.5 -90.5z" />
+<glyph unicode="&#xf404;" d="M0 1024l506 506q101 103 234.5 160.5t283.5 57.5q193 0 358 -95l-143 -143q-103 46 -215 46q-108 0 -206.5 -41t-173.5 -119l-372 -372l240 -240l-136 -136zM339 429l90 -90l1280 1280l-90 90zM640 1024q0 159 112.5 271.5t271.5 112.5q44 0 98 -14l-468 -468 q-14 54 -14 98zM666 395l143 143q103 -46 215 -46q108 0 206 41.5t174 118.5l373 372l-241 241l136 135l376 -376l-506 -507q-101 -103 -234.5 -160t-283.5 -57q-193 0 -358 95zM926 654l468 468q14 -54 14 -98q0 -159 -112.5 -271.5t-271.5 -112.5q-44 0 -98 14z" />
+<glyph unicode="&#xf405;" d="M640 768l320 320l-320 320l128 128l320 -320l320 320l128 -128l-320 -320l320 -320l-128 -128l-320 320l-320 -320z" />
+<glyph unicode="&#xf406;" d="M128 256l832 832l-832 832l128 128l832 -832l832 832l128 -128l-832 -832l832 -832l-128 -128l-832 832l-832 -832z" />
+<glyph unicode="&#xf407;" d="M384 1280q0 106 75 181t181 75h140q20 56 69.5 92t110.5 36q62 0 111 -35.5t69 -92.5h140q106 0 181 -75t75 -181h-128v-768q0 -53 -37.5 -90.5t-90.5 -37.5h-640q-53 0 -90.5 37.5t-37.5 90.5v768h-128zM640 576q0 -26 19 -45t45 -19t45 19t19 45v640q0 26 -19 45 t-45 19t-45 -19t-19 -45v-640zM896 576q0 -26 19 -45t45 -19t45 19t19 45v640q0 26 -19 45t-45 19t-45 -19t-19 -45v-640zM1152 576q0 -26 19 -45t45 -19t45 19t19 45v640q0 26 -19 45t-45 19t-45 -19t-19 -45v-640z" />
+<glyph unicode="&#xf408;" d="M256 1151l476 -330l-183 -535l475 332l475 -332l-183 535l476 329h-587l-181 535l-180 -534h-588z" />
+<glyph unicode="&#xf409;" d="M384 1024l640 640l640 -640l-128 -128l-512 512l-512 -512zM640 512v384l384 384l384 -384v-384h-256v384h-256v-384h-256z" />
+<glyph unicode="&#xf410;" d="M0 384l640 640l384 -384l384 384l640 -640h-2048zM0 512v1152l576 -576zM0 1792h2048l-1024 -1024zM1472 1088l576 576v-1152z" />
+<glyph unicode="&#xf411;" d="M384 384v448l896 896l448 -448l-896 -896h-448zM512 768l256 -256l128 128l-256 256zM685 941l96 -96l595 595l-96 96zM845 781l96 -96l595 595l-96 96z" />
+<glyph unicode="&#xf412;" d="M256 640v704l384 384v-704h640v448l640 -640l-640 -640v448h-1024z" />
+<glyph unicode="&#xf413;" d="M128 384q0 106 75 181t181 75t181 -75t75 -181t-75 -181t-181 -75t-181 75t-75 181zM128 971v345q240 0 459 -94t377.5 -253.5t252.5 -379.5t94 -461h-345q0 170 -63.5 324t-181.5 273q-119 119 -272 182.5t-321 63.5zM129 1582v345q243 0 475 -64.5t428.5 -181 t362 -282.5t281 -363.5t180 -430.5t64.5 -477h-345q0 197 -52 385.5t-145.5 348t-227 294t-292 228t-346 146t-383.5 52.5z" />
+<glyph unicode="&#xf414;" d="M21 230q-57 102 31 244l760 1237q57 93 134.5 126.5t155 0t135.5 -126.5l759 -1237q88 -142 31 -244t-224 -102h-1557q-168 0 -225 102zM896 512q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5t-37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5zM896 896 q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5v384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5v-384z" />
+<glyph unicode="&#xf415;" d="M128 1024h400q45 0 79.5 27.5t44.5 69.5q33 125 136.5 206t235.5 81q154 0 270 -114q38 -38 90.5 -38t90.5 38q37 38 37 91t-37 90q-88 89 -204.5 139t-246.5 50q-194 0 -353 -106t-234 -278h-309v-256zM536 663q0 -53 37 -90q89 -89 205 -139t246 -50q194 0 353 106 t234 278h309v256h-400q-45 0 -79.5 -27.5t-44.5 -69.5q-33 -125 -136.5 -206t-235.5 -81q-156 0 -269 115q-38 37 -91 37t-91 -38q-37 -38 -37 -91zM768 1024q0 -106 75 -181t181 -75t181 75t75 181t-75 181t-181 75t-181 -75t-75 -181z" />
+<glyph unicode="&#xf416;" d="M512 832v320h128v-320q0 -133 93.5 -226.5t226.5 -93.5t226.5 93.5t93.5 226.5v640q0 80 -56 136t-136 56t-136 -56t-56 -136v-512q0 -26 19 -45t45 -19t45 19t19 45v452h128v-452q0 -80 -56 -136t-136 -56t-136 56t-56 136v512q0 133 93.5 226.5t226.5 93.5t226.5 -93.5 t93.5 -226.5v-640q0 -185 -131.5 -316.5t-316.5 -131.5t-316.5 131.5t-131.5 316.5z" />
+<glyph unicode="&#xf417;" d="M384 1216q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5t-44.5 -222.5t-124.5 -185.5l-407 -406l-407 406q-80 80 -124.5 185.5t-44.5 222.5zM640 1216q0 -133 93.5 -226.5t226.5 -93.5t226.5 93.5t93.5 226.5t-93.5 226.5 t-226.5 93.5t-226.5 -93.5t-93.5 -226.5z" />
+<glyph unicode="&#xf418;" d="M608 1056l128 128l224 -192l448 512l128 -96l-512 -768h-128z" />
+<glyph unicode="&#xf419;" d="M0 256v256h2048v-256h-2048zM0 896v256h2048v-256h-2048zM0 1536v256h2048v-256h-2048z" />
+<glyph unicode="&#xf420;" d="M384 512l640 640l640 -640h-1280zM384 1280v128h1280v-128h-1280z" />
+<glyph unicode="&#xf421;" d="M384 896v256h1152v-256h-1152z" />
+<glyph unicode="&#xf422;" d="M384 512v1024h1152v-1024h-1152zM512 640h896v640h-896v-640z" />
+<glyph unicode="&#xf500;" d="M128 0l960 960l960 -960h-1920z" />
+<glyph unicode="&#xf501;" d="M0 128l960 960l-960 960v-1920z" />
+<glyph unicode="&#xf502;" d="M128 2048l960 -960l960 960h-1920z" />
+<glyph unicode="&#xf503;" d="M1088 1088l960 960v-1920z" />
+</font>
+</defs></svg> \ No newline at end of file
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.ttf b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.ttf
new file mode 100644
index 00000000..45228d9e
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.ttf
Binary files differ
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.woff b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.woff
new file mode 100644
index 00000000..a64be4b1
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/fonts/genericons-regular-webfont.woff
Binary files differ
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/template-tags.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/template-tags.php
new file mode 100644
index 00000000..6a398391
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/template-tags.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * Custom template tags for this theme.
+ *
+ * Eventually, some of the functionality here could be replaced by core features
+ *
+ * @package Minileven
+ * @since Minileven 2.0
+ */
+
+/**
+ * Display navigation to next/previous pages when applicable
+ */
+function minileven_content_nav( $nav_id ) {
+ global $wp_query;
+
+ if ( $wp_query->max_num_pages > 1 ) : ?>
+ <nav id="<?php echo $nav_id; ?>">
+ <h3 class="assistive-text"><?php _e( 'Post navigation', 'jetpack' ); ?></h3>
+ <div class="nav-previous"><?php next_posts_link( __( '<span class="meta-nav">&laquo;</span> Older', 'jetpack' ) ); ?></div>
+ <div class="nav-next"><?php previous_posts_link( __( 'Newer <span class="meta-nav">&raquo;</span>', 'jetpack' ) ); ?></div>
+ </nav><!-- #nav-above -->
+ <?php endif;
+}
+
+/**
+ * Template for comments and pingbacks.
+ * Used as a callback by wp_list_comments() for displaying the comments.
+ * @since Minileven 1.0
+ */
+function minileven_comment( $comment, $args, $depth ) {
+ $GLOBALS['comment'] = $comment;
+ switch ( $comment->comment_type ) :
+ case 'pingback' :
+ case 'trackback' :
+ ?>
+ <li class="post pingback">
+ <p><?php _e( 'Pingback:', 'jetpack' ); ?> <?php comment_author_link(); ?></p>
+ <?php
+ break;
+ default :
+ ?>
+ <li <?php comment_class(); ?> id="li-comment-<?php comment_ID(); ?>">
+ <article id="comment-<?php comment_ID(); ?>" class="comment">
+ <footer class="comment-meta">
+ <div class="comment-author vcard">
+ <?php
+ $avatar_size = 32;
+ if ( '0' != $comment->comment_parent )
+ $avatar_size = 24;
+
+ echo get_avatar( $comment, $avatar_size );
+
+ /* translators: 1: comment author, 2: date and time */
+ printf( __( '%1$s on %2$s', 'jetpack' ),
+ sprintf( '<span class="fn">%s</span>', get_comment_author_link() ),
+ sprintf( '<a href="%1$s"><time pubdate datetime="%2$s">%3$s</time></a>',
+ esc_url( get_comment_link( $comment->comment_ID ) ),
+ get_comment_time( 'c' ),
+ /* translators: 1: date, 2: time */
+ sprintf( __( '%1$s at %2$s', 'jetpack' ), get_comment_date(), get_comment_time() )
+ )
+ );
+ ?>
+ </div><!-- .comment-author .vcard -->
+
+ <?php if ( $comment->comment_approved == '0' ) : ?>
+ <em class="comment-awaiting-moderation"><?php _e( 'Your comment is awaiting moderation.', 'jetpack' ); ?></em>
+ <br />
+ <?php endif; ?>
+
+ </footer>
+
+ <div class="comment-content"><?php comment_text(); ?></div>
+ <div class="reply">
+ <?php comment_reply_link( array_merge( $args, array( 'reply_text' => __( 'Reply', 'jetpack' ), 'depth' => $depth, 'max_depth' => $args['max_depth'] ) ) ); ?>
+ </div><!-- .reply -->
+ </article><!-- #comment-## -->
+
+ <?php
+ break;
+ endswitch;
+}
+
+/**
+ * Prints HTML with meta information for the current post-date/time and author.
+ * @since Minileven 1.0
+ */
+function minileven_posted_on() {
+ printf( __( '<span class="entry-date"><a href="%1$s" title="%2$s" rel="bookmark"><time datetime="%3$s" pubdate>%4$s</time></a></span>', 'jetpack' ),
+ esc_url( get_permalink() ),
+ esc_attr( get_the_time() ),
+ esc_attr( get_the_date( 'c' ) ),
+ esc_html( get_the_date() )
+ );
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/tweaks.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/tweaks.php
new file mode 100644
index 00000000..a0c664f5
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/inc/tweaks.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * Custom functions that act independently of the theme templates
+ *
+ * Eventually, some of the functionality here could be replaced by core features
+ *
+ * @package Minileven
+ * @since Minileven 2.0
+ */
+
+/**
+* Sets the post excerpt length based on number of characters, without breaking words at the end
+*
+*/
+function minileven_excerpt( $count ) {
+ $excerpt = get_the_content();
+ $excerpt = strip_tags( $excerpt );
+ $excerpt = strip_shortcodes( $excerpt );
+ $excerpt = substr( $excerpt, 0, $count );
+ $excerpt = substr( $excerpt, 0, strripos( $excerpt, " " ) );
+ $excerpt = $excerpt . minileven_continue_reading_link();
+ return $excerpt;
+}
+/**
+
+/**
+ * Returns a "Continue Reading" link for excerpts
+ */
+function minileven_continue_reading_link() {
+ return ' &hellip; <a href="'. esc_url( get_permalink() ) . '">' . __( 'Continue reading <span class="meta-nav">&rarr;</span>', 'jetpack' ) . '</a>';
+}
+
+/**
+ * Replaces "[...]" (appended to automatically generated excerpts) with an ellipsis and minileven_continue_reading_link().
+ */
+function minileven_auto_excerpt_more( $more ) {
+ return ' &hellip;' . minileven_continue_reading_link();
+}
+add_filter( 'excerpt_more', 'minileven_auto_excerpt_more' );
+
+/**
+ * Adds a pretty "Continue Reading" link to custom post excerpts.
+ *
+ * To override this link in a child theme, remove the filter and add your own
+ * function tied to the get_the_excerpt filter hook.
+ */
+function minileven_custom_excerpt_more( $output ) {
+ if ( has_excerpt() && ! is_attachment() ) {
+ $output .= minileven_continue_reading_link();
+ }
+ return $output;
+}
+add_filter( 'get_the_excerpt', 'minileven_custom_excerpt_more' );
+
+/**
+ * Get our wp_nav_menu() fallback, wp_page_menu(), to show a home link.
+ */
+function minileven_page_menu_args( $args ) {
+ $args['show_home'] = true;
+ return $args;
+}
+add_filter( 'wp_page_menu_args', 'minileven_page_menu_args' );
+
+/**
+ * Adds a custom class to the array of body classes, to allow Minileven to be targeted with Custom CSS.
+ */
+function minileven_body_classes( $classes ) {
+ $classes[] = 'mobile-theme';
+ return $classes;
+}
+add_filter( 'body_class', 'minileven_body_classes' );
+
+/**
+ * Filters wp_title to print a neat <title> tag based on what is being viewed.
+ *
+ * @since Minileven 2.0
+ */
+function minileven_wp_title( $title, $sep ) {
+ global $page, $paged;
+
+ if ( is_feed() )
+ return $title;
+
+ // Add the blog name
+ $title .= get_bloginfo( 'name' );
+
+ // Add the blog description for the home/front page.
+ $site_description = get_bloginfo( 'description', 'display' );
+ if ( $site_description && ( is_home() || is_front_page() ) )
+ $title .= " $sep $site_description";
+
+ // Add a page number if necessary:
+ if ( $paged >= 2 || $page >= 2 )
+ $title .= " $sep " . sprintf( __( 'Page %s', 'jetpack' ), max( $paged, $page ) );
+
+ return $title;
+}
+add_filter( 'wp_title', 'minileven_wp_title', 10, 2 );
+
+/**
+ * Add theme support for Responsive Videos.
+ */
+add_theme_support( 'jetpack-responsive-videos' ); \ No newline at end of file
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/index.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/index.php
new file mode 100644
index 00000000..63a51a19
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/index.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * The main template file.
+ *
+ * This is the most generic template file in a WordPress theme
+ * and one of the two required files for a theme (the other being style.css).
+ * It is used to display a page when nothing more specific matches a query.
+ * E.g., it puts together the home page when no home.php file exists.
+ * Learn more: http://codex.wordpress.org/Template_Hierarchy
+ *
+ * @package Minileven
+ */
+
+get_header(); ?>
+
+ <div id="primary">
+ <div id="content" role="main">
+
+ <?php if ( is_archive() ) : ?>
+ <header class="page-header">
+ <h1 class="page-title">
+ <?php if ( is_day() ) : ?>
+ <?php printf( __( 'Daily Archives: %s', 'jetpack' ), '<span>' . get_the_date() . '</span>' ); ?>
+ <?php elseif ( is_month() ) : ?>
+ <?php printf( __( 'Monthly Archives: %s', 'jetpack' ), '<span>' . get_the_date( 'F Y' ) . '</span>' ); ?>
+ <?php elseif ( is_year() ) : ?>
+ <?php printf( __( 'Yearly Archives: %s', 'jetpack' ), '<span>' . get_the_date( 'Y' ) . '</span>' ); ?>
+ <?php elseif ( is_category() ) : ?>
+ <?php printf( __( 'Posted in %s', 'jetpack' ), '<span>' . single_cat_title( '', false ) . '</span>' ); ?>
+ <?php elseif ( is_tag() ) : ?>
+ <?php printf( __( 'Tagged with %s', 'jetpack' ), '<span>' . single_tag_title( '', false ) . '</span>' ); ?>
+ <?php elseif( is_author() ) : ?>
+ <?php printf( __( 'Posted by %s', 'jetpack' ), '<span>' . get_the_author() . '</span>' ); ?>
+ <?php else : ?>
+ <?php _e( 'Blog Archives', 'jetpack' ); ?>
+ <?php endif; ?>
+ </h1>
+ </header>
+ <?php endif; ?>
+
+ <?php if ( is_search() ) : ?>
+ <header class="page-header">
+ <h1 class="page-title"><?php printf( __( 'Search Results for: %s', 'jetpack' ), '<span>' . get_search_query() . '</span>' ); ?></h1>
+ </header>
+ <?php endif; ?>
+
+ <?php if ( have_posts() ) : // Start the loop ?>
+ <?php while ( have_posts() ) : the_post(); ?>
+
+ <?php get_template_part( 'content', get_post_format() ); ?>
+
+ <?php endwhile; ?>
+
+ <?php else : ?>
+ <article id="post-0" class="post error404 not-found">
+ <header class="entry-header">
+ <h1 class="entry-title"><?php _e( 'Nothing Found', 'jetpack' ); ?></h1>
+ </header><!-- .entry-header -->
+
+ <div class="entry-content">
+ <p><?php _e( 'Apologies, but no results were found for the requested archive. Perhaps searching will help find a related post.', 'jetpack' ); ?></p>
+ <?php get_search_form(); ?>
+ </div><!-- .entry-content -->
+ </article><!-- #post-0 -->
+
+ <?php endif; ?>
+
+ </div><!-- #content -->
+
+ <?php minileven_content_nav( 'nav-below' ); ?>
+
+ </div><!-- #primary -->
+
+<?php get_sidebar(); ?>
+<?php get_footer(); ?>
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/js/small-menu.js b/plugins/jetpack/modules/minileven/theme/pub/minileven/js/small-menu.js
new file mode 100644
index 00000000..ee3c7a4d
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/js/small-menu.js
@@ -0,0 +1,38 @@
+/**
+ * navigation.js
+ *
+ * Handles toggling the navigation menu for small screens.
+ */
+( function() {
+ var nav = document.getElementById( 'access' ),
+ button,
+ menu;
+ if ( ! nav ) {
+ return;
+ }
+ button = nav.getElementsByTagName( 'h3' )[ 0 ];
+ menu = nav.getElementsByTagName( 'ul' )[ 0 ];
+ if ( ! button ) {
+ return;
+ }
+
+ // Hide button if menu is missing or empty.
+ if ( ! menu || ! menu.childNodes.length ) {
+ button.style.display = 'none';
+ return;
+ }
+
+ button.onclick = function() {
+ if ( -1 === menu.className.indexOf( 'nav-menu' ) ) {
+ menu.className = 'nav-menu';
+ }
+
+ if ( -1 !== button.className.indexOf( 'toggled-on' ) ) {
+ button.className = button.className.replace( ' toggled-on', '' );
+ menu.className = menu.className.replace( ' toggled-on', '' );
+ } else {
+ button.className += ' toggled-on';
+ menu.className += ' toggled-on';
+ }
+ };
+} )();
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/page.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/page.php
new file mode 100644
index 00000000..0b09197b
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/page.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * The template for displaying all pages.
+ *
+ * This is the template that displays all pages by default.
+ * Please note that this is the WordPress construct of pages
+ * and that other 'pages' on your WordPress site will use a
+ * different template.
+ *
+ * @package Minileven
+ */
+
+get_header(); ?>
+
+ <div id="primary">
+ <div id="content" role="main">
+
+ <?php while ( have_posts() ) : the_post(); ?>
+
+ <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
+ <header class="entry-header">
+ <?php if ( '1' == get_option( 'wp_mobile_featured_images' ) && minileven_show_featured_images() ) : ?>
+ <div class="entry-thumbnail">
+ <a href="<?php the_permalink(); ?>" title="<?php echo esc_attr( sprintf( __( 'Permalink to %s', 'jetpack' ), the_title_attribute( 'echo=0' ) ) ); ?>" rel="<?php the_ID(); ?>" class="minileven-featured-thumbnail"><?php the_post_thumbnail(); ?></a>
+ </div><!-- .entry-thumbnail -->
+ <?php endif; ?>
+ <h1 class="entry-title"><?php the_title(); ?></h1>
+ </header><!-- .entry-header -->
+
+ <div class="entry-content">
+ <?php the_content(); ?>
+ <?php wp_link_pages( array( 'before' => '<div class="page-link"><span>' . __( 'Pages:', 'jetpack' ) . '</span>', 'after' => '</div>' ) ); ?>
+ </div><!-- .entry-content -->
+ <?php if ( is_user_logged_in() ) : ?>
+ <footer class="entry-meta">
+ <?php edit_post_link( __( 'Edit', 'jetpack' ), '<span class="edit-link">', '</span>' ); ?>
+ </footer><!-- .entry-meta -->
+ <?php endif; ?>
+ </article><!-- #post-<?php the_ID(); ?> -->
+
+ <?php comments_template( '', true ); ?>
+
+ <?php endwhile; // end of the loop. ?>
+
+ </div><!-- #content -->
+ </div><!-- #primary -->
+<?php get_footer(); ?> \ No newline at end of file
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/rtl.css b/plugins/jetpack/modules/minileven/theme/pub/minileven/rtl.css
new file mode 100644
index 00000000..b2839d74
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/rtl.css
@@ -0,0 +1,574 @@
+/*
+Theme Name: Twenty Eleven
+
+Adding support for language written in a Right To Left (RTL) direction is easy -
+it's just a matter of overwriting all the horizontal positioning attributes
+of your CSS stylesheet in a separate stylesheet file named rtl.css.
+
+http://codex.wordpress.org/Right_to_Left_Language_Support
+
+*/
+
+/* =Reset reset
+----------------------------------------------- */
+
+caption, th, td {
+ text-align: right;
+}
+
+/* =Structure
+----------------------------------------------- */
+
+body {
+ direction:rtl;
+ unicode-bidi:embed;
+}
+
+/* Showcase */
+.page-template-showcase-php section.recent-posts {
+ float: left;
+ margin: 0 31% 0 0;
+}
+.page-template-showcase-php #main .widget-area {
+ float: right;
+ margin: 0 0 0 -22.15%;
+}
+
+/* One column */
+
+.one-column article.feature-image.small .entry-summary a {
+ left: auto;
+ right: -9%;
+}
+
+/* Simplify the pullquotes and pull styles */
+.one-column.singular .entry-meta .edit-link a {
+ right: 0px;
+ left: auto;
+}
+/* Make sure we have room for our comment avatars */
+.one-column .commentlist > li.comment {
+ margin-left: 0;
+ margin-right: 102px;
+}
+/* Make sure the logo and search form don't collide */
+.one-column #branding #searchform {
+ right: auto;
+ left: 40px;
+}
+/* Talking avatars take up too much room at this size */
+.one-column .commentlist > li.comment {
+ margin-right: 0;
+}
+.one-column .commentlist > li.comment .comment-meta,
+.one-column .commentlist > li.comment .comment-content {
+ margin-right: 0;
+ margin-left: 85px;
+}
+.one-column .commentlist .avatar {
+ right: auto;
+ left: 1.625em;
+}
+.one-column .commentlist .children .avatar {
+ left: auto;
+ right: 2.2em;
+}
+
+/* =Global
+----------------------------------------------- */
+
+/* Text elements */
+p {
+ margin-bottom: 1.625em;
+}
+ul, ol {
+ margin: 0 2.5em 1.625em 0;
+}
+.ltr ul, ol {
+ margin: 0 0 1.625em 2.5em;
+}
+blockquote {
+ font-family: Arial, sans-serif;
+}
+blockquote em, blockquote i, blockquote cite {
+ font-style: normal;
+}
+
+/* Forms */
+textarea {
+ padding-left: 0;
+ padding-right: 3px;
+}
+input#s {
+ background-position: 97% 6px;
+ padding: 4px 28px 4px 10px;
+}
+
+/* Assistive text */
+#access a.assistive-text:active,
+#access a.assistive-text:focus {
+ left: auto;
+ right: 7.6%;
+}
+
+/* =Header
+----------------------------------------------- */
+
+#site-title {
+ margin-right: 0;
+ margin-left: 270px;
+}
+
+#site-description {
+ margin: 0 0 3.65625em 270px;
+}
+
+/* =Menu
+-------------------------------------------------------------- */
+
+#access {
+ float: right;
+}
+#access ul {
+ margin: 0 -0.8125em 0 0;
+ padding-right: 0;
+}
+#access li {
+ float: right;
+}
+#access ul ul {
+ float: right;
+ left: auto;
+ right: 0;
+}
+#access ul ul ul {
+ left: auto;
+ right: 100%;
+}
+
+/* Search Form */
+#branding #searchform {
+ right: auto;
+ left: 7.6%;
+ text-align: left;
+}
+#branding #s {
+ float: left;
+}
+#branding .only-search + #access div {
+ padding-right: 0;
+ padding-left: 205px;
+}
+
+
+/* =Content
+----------------------------------------------- */
+.entry-title,
+.entry-header .entry-meta {
+ padding-right: 0;
+ padding-left: 76px;
+}
+.entry-content td,
+.comment-content td {
+ padding: 6px 0 6px 10px;
+}
+.page-link span {
+ margin-right: 0;
+ margin-left: 6px;
+}
+.entry-meta .edit-link a {
+ float: left;
+}
+/* Images */
+
+.wp-caption .wp-caption-text,
+.gallery-caption {
+ font-family: Arial, sans-serif;
+}
+.wp-caption .wp-caption-text {
+ padding: 10px 40px 5px 0px;
+}
+.wp-caption .wp-caption-text:before {
+ margin-right: 0;
+ margin-left: 5px;
+ left: auto;
+ right: 10px;
+}
+#content .gallery-columns-4 .gallery-item {
+ padding-right:0;
+ padding-left:2%;
+}
+
+/* Author Info */
+.singular #author-info {
+ margin: 2.2em -35.4% 0 -35.6%;
+}
+#author-avatar {
+ float: right;
+ margin-right: 0;
+ margin-left: -78px;
+}
+#author-description {
+ float: right;
+ margin-left: 0;
+ margin-right: 108px;
+}
+/* Comments link */
+.entry-header .comments-link a {
+ background-image: url(images/comment-bubble-rtl.png);
+ right: auto;
+ left: 0;
+}
+
+/*
+ Post Formats Headings
+*/
+.singular .entry-title,
+.singular .entry-header .entry-meta {
+ padding-left: 0;
+}
+.singular .entry-header .entry-meta {
+ left: auto;
+ right: 0;
+}
+.singular .entry-meta .edit-link a {
+ left: auto;
+ right: 50px;
+}
+
+
+/* =Gallery
+----------------------------------------------- */
+
+.format-gallery .gallery-thumb {
+ float: right;
+ margin: .375em 0 0 1.625em;
+}
+
+
+/* =Status
+----------------------------------------------- */
+
+.format-status img.avatar {
+ float: right;
+ margin: 4px 0 2px 10px;
+}
+
+
+/* =Image
+----------------------------------------------- */
+
+.indexed.format-image div.entry-meta {
+ float: right;
+}
+/* =error404
+----------------------
+------------------------- */
+.error404 #main .widget {
+ float: right;
+ margin-right: auto;
+ margin-left: 3.7%;
+}
+.error404 #main .widget_archive {
+ margin-left: 0;
+}
+.error404 #main .widget_tag_cloud {
+ margin-left: 0;
+}
+
+/* =Showcase
+----------------------------------------------- */
+
+article.intro .edit-link a {
+ right: auto;
+ left: 20px;
+}
+
+/* Featured post */
+section.featured-post {
+ float: right;
+}
+
+/* Small featured post */
+section.featured-post .attachment-small-feature {
+ float: left;
+ margin: 0 0 1.625em -8.9%;
+ right: auto;
+ left: -15px;
+}
+article.feature-image.small {
+ float: right;
+}
+article.feature-image.small .entry-summary p a {
+ left:auto;
+ right: -23.8%;
+ padding: 9px 85px 9px 26px;
+}
+
+/* Large featured post */
+section.feature-image.large .hentry {
+ left:auto;
+ right: 9%;
+ margin: 1.625em 0 0 9%;
+}
+/* Featured Slider */
+.featured-posts .showcase-heading {
+ padding-left: 0;
+ padding-right: 8.9%;
+}
+.featured-posts section.featured-post {
+ left: auto;
+ right: 0;
+}
+#content .feature-slider {
+ right: auto;
+ left: 8.9%;
+}
+.feature-slider li {
+ float: right;
+}
+/* Recent Posts */
+section.recent-posts .other-recent-posts a[rel="bookmark"] {
+ float: right;
+}
+section.recent-posts .other-recent-posts .comments-link a,
+section.recent-posts .other-recent-posts .comments-link > span {
+ padding: 0.3125em 1em 0.3125em 0;
+ left: 0;
+ text-align: left;
+}
+
+/* =Attachments
+----------------------------------------------- */
+
+/* =Navigation
+-------------------------------------------------------------- */
+
+.nav-previous {
+ float: right;
+}
+.nav-next {
+ float: left;
+ text-align: left;
+}
+
+/* Singular navigation */
+#nav-single {
+ float: left;
+ text-align: left;
+}
+#nav-single .nav-next {
+ padding-left: 0;
+ padding-right: .5em;
+}
+
+
+/* =Widgets
+----------------------------------------------- */
+
+.widget ul ul {
+ margin-left: 0;
+ margin-right: 1.5em;
+}
+
+/* Twitter */
+.widget_twitter .timesince {
+ margin-right: 0;
+ margin-left: -10px;
+ text-align: left;
+}
+
+/* =Comments
+----------------------------------------------- */
+
+.commentlist .children li.comment {
+ border-left: none;
+ border-right: 1px solid #ddd;
+ -moz-border-radius: 3px 0 0 3px;
+ border-radius: 3px 0 0 3px;
+}
+.commentlist .children li.comment .comment-meta {
+ margin-left: 0;
+ margin-right: 50px;
+}
+.commentlist .avatar {
+ left: auto;
+ right: -102px;
+}
+.commentlist > li:before {
+ content: url(images/comment-arrow-rtl.png);
+ left:auto;
+ right: -21px;
+}
+.commentlist > li.pingback:before {
+ content: '';
+}
+.commentlist .children .avatar {
+ left: auto;
+ right: 2.2em;
+}
+
+/* Post author highlighting */
+.commentlist > li.bypostauthor:before {
+ content: url(images/comment-arrow-bypostauthor-rtl.png);
+}
+
+/* sidebar-page.php comments */
+/* Make sure we have room for our comment avatars */
+.page-template-sidebar-page-php .commentlist > li.comment,
+.page-template-sidebar-page-php.commentlist .pingback {
+ margin-left: 0;
+ margin-right: 102px;
+}
+
+/* Comment Form */
+#respond .comment-form-author label,
+#respond .comment-form-email label,
+#respond .comment-form-url label,
+#respond .comment-form-comment label {
+ left: auto;
+ right: 4px;
+}
+#respond .comment-form-author label,
+#respond .comment-form-email label,
+#respond .comment-form-url label,
+#respond .comment-form-comment label {
+ -webkit-box-shadow: -1px 2px 2px rgba(204,204,204,0.8);
+ -moz-box-shadow: -1px 2px 2px rgba(204,204,204,0.8);
+ box-shadow: -1px 2px 2px rgba(204,204,204,0.8);
+}
+#respond .comment-form-author .required,
+#respond .comment-form-email .required {
+ left: auto;
+ right: 75%;
+}
+#respond .form-submit {
+ float: left;
+}
+#respond input#submit {
+ left: auto;
+ right: 30px;
+ padding: 5px 22px 5px 42px;
+}
+#respond #cancel-comment-reply-link {
+ margin-left: 0;
+ margin-right: 10px;
+}
+#cancel-comment-reply-link {
+ right: auto;
+ left: 1.625em;
+}
+
+/* =Footer
+----------------------------------------------- */
+
+/* Two Footer Widget Areas */
+#supplementary.two .widget-area {
+ float: right;
+ margin-right: 0;
+ margin-left: 3.7%;
+}
+#supplementary.two .widget-area + .widget-area {
+ margin-left: 0;
+}
+
+/* Three Footer Widget Areas */
+#supplementary.three .widget-area {
+ float: right;
+ margin-right: 0;
+ margin-left: 3.7%;
+}
+#supplementary.three .widget-area + .widget-area + .widget-area {
+ margin-left: 0;
+}
+
+/* Site Generator Line */
+#site-generator .sep {
+ background-position: right center;
+}
+
+
+/* =Responsive Structure
+----------------------------------------------- */
+
+@media (max-width: 800px) {
+ /* Simplify the showcase template when small feature */
+ section.featured-post .attachment-small-feature,
+ .one-column section.featured-post .attachment-small-feature {
+ float: right;
+ }
+ article.feature-image.small {
+ float: left;
+ }
+ article.feature-image.small .entry-summary p a {
+ right: 0;
+ }
+ .singular .entry-meta .edit-link a {
+ left: auto;
+ right: 0px;
+ }
+ /* Make sure we have room for our comment avatars */
+ .commentlist > li.comment,
+ .commentlist .pingback {
+ margin-left: 0;
+ margin-right: 102px;
+ }
+ /* No need to float footer widgets at this size */
+ #colophon #supplementary .widget-area {
+ margin-left: 0;
+ }
+ /* No need to float 404 widgets at this size */
+ .error404 #main .widget {
+ margin-left: 0;
+ }
+}
+@media (max-width: 650px) {
+ /* @media (max-width: 650px) Reduce font-sizes for better readability on smaller devices */
+ #site-title,
+ #site-description {
+ margin-left: 0;
+ }
+ /* Talking avatars take up too much room at this size */
+ .commentlist > li.comment,
+ .commentlist > li.pingback {
+ margin-right: 0 !important;
+ }
+ .commentlist .children .avatar {
+ left: auto;
+ right: 2.2em;
+ }
+ /* Use the available space in the smaller comment form */
+ #respond .comment-form-author .required,
+ #respond .comment-form-email .required {
+ left: auto;
+ right: 95%;
+ }
+ #content .gallery-columns-3 .gallery-item {
+ padding-right: 0;
+ padding-left:2%;
+ }
+}
+@media (max-width: 450px) {
+ #content .gallery-columns-2 .gallery-item {
+ padding-right:0;
+ padding-left:4%;
+ }
+}
+
+/* =Print
+----------------------------------------------- */
+
+@media print {
+ #primary {
+ float: right;
+ }
+ /* Comments */
+ .commentlist .avatar {
+ left: auto;
+ right: 2.2em;
+ }
+ .commentlist li.comment .comment-meta {
+ margin-left: 0;
+ margin-right: 50px;
+ }
+}
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/screenshot.png b/plugins/jetpack/modules/minileven/theme/pub/minileven/screenshot.png
new file mode 100644
index 00000000..d735057f
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/screenshot.png
Binary files differ
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/searchform.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/searchform.php
new file mode 100644
index 00000000..b4fd79aa
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/searchform.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * The template for displaying search forms in Minileven
+ *
+ * @package Minileven
+ */
+?>
+ <form method="get" id="searchform" action="<?php echo esc_url( home_url( '/' ) ); ?>">
+ <label for="s" class="assistive-text"><?php _e( 'Search', 'jetpack' ); ?></label>
+ <input type="text" class="field" name="s" id="s" placeholder="<?php esc_attr_e( 'Search', 'jetpack' ); ?>" />
+ <input type="submit" class="submit" name="submit" id="searchsubmit" value="<?php esc_attr_e( 'Search', 'jetpack' ); ?>" />
+ </form>
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/sidebar.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/sidebar.php
new file mode 100644
index 00000000..5d1a4a06
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/sidebar.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * The Sidebar containing the main widget area.
+ *
+ * @package Minileven
+ */
+?>
+ <?php if ( is_active_sidebar( 'sidebar-1' ) ) : ?>
+ <div id="secondary" class="widget-area" role="complementary">
+ <?php dynamic_sidebar( 'sidebar-1' ); ?>
+ </div><!-- #secondary .widget-area -->
+ <?php endif; ?> \ No newline at end of file
diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/style.css b/plugins/jetpack/modules/minileven/theme/pub/minileven/style.css
new file mode 100644
index 00000000..d3727dc0
--- /dev/null
+++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/style.css
@@ -0,0 +1,1588 @@
+/*
+Theme Name: Minileven
+Theme URI: http://theme.wordpress.com
+Author: Automattic
+Author URI: http://theme.wordpress.com
+Description: The Minileven theme is a clean, lightweight mobile experience for your blog based on Twenty Eleven.
+Version: 2.0-wpcom
+License: GNU General Public License
+License URI: license.txt
+Tags: dark, light, white, black, gray, one-column, fluid-layout, responsive-layout, custom-background, custom-header, custom-menu, full-width-template, infinite-scroll, microformats, post-formats, rtl-language-support, sticky-post, theme-options, translation-ready, blog, bright, clean, contemporary, elegant, minimal, modern, photography, simple, tumblelog
+*/
+
+.image-attachment .entry-caption p {
+ font-size: 0.769em;
+ letter-spacing: 0.1em;
+ line-height: 2.6;
+ margin: 0 0 2.6em;
+ text-transform: uppercase;
+}
+
+/* =Webfont, thanks to FontSquirrel.com for conversion!
+-------------------------------------------------------------- */
+@font-face {
+ font-family: 'Genericons';
+ src: url('inc/fonts/genericons-regular-webfont.eot');
+ src: url('inc/fonts/genericons-regular-webfont.eot?#iefix') format('embedded-opentype'),
+ url('inc/fonts/genericons-regular-webfont.woff') format('woff'),
+ url('inc/fonts/genericons-regular-webfont.ttf') format('truetype'),
+ url('inc/fonts/genericons-regular-webfont.svg#genericonsregular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+/* =Reset default browser CSS. Based on work by Eric Meyer: http://meyerweb.com/eric/tools/css/reset/index.html
+-------------------------------------------------------------- */
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td {
+ border: 0;
+ font-family: inherit;
+ font-size: 100%;
+ font-style: inherit;
+ font-weight: inherit;
+ margin: 0;
+ outline: 0;
+ padding: 0;
+ vertical-align: baseline;
+}
+:focus {/* remember to define focus styles! */
+ outline: 0;
+}
+body {
+ background: #fff;
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+a img {
+ border: 0;
+}
+article, aside, details, figcaption, figure,
+footer, header, menu, nav, section {
+ display: block;
+}
+
+
+/* =Global
+----------------------------------------------- */
+
+body, input, textarea {
+ color: #404040;
+ font: 13px "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ line-height: 1.625;
+ word-wrap: break-word;
+}
+body {
+ background: #f1f1f1;
+ font-weight: 400;
+}
+#page,
+#branding {
+ background: #fff;
+ -moz-box-shadow: 0 1px 2px rgba( 0,0,0,0.075 );
+ -webkit-box-shadow: 0 1px 2px rgba( 0,0,0,0.075 );
+ box-shadow: 0 1px 2px rgba( 0,0,0,0.075 );
+}
+
+/* Headings */
+h1,h2,h3,h4,h5,h6 {
+ clear: both;
+}
+hr {
+ background-color: #ccc;
+ border: 0;
+ height: 1px;
+ margin-bottom: 1.625em;
+}
+
+/* Text elements */
+p {
+ margin-bottom: 1.0em;
+}
+ul, ol {
+ margin: 0 0 1.625em 2.5em;
+}
+ul {
+ list-style: square;
+}
+ol {
+ list-style-type: decimal;
+}
+ul ul, ol ol, ul ol, ol ul {
+ margin-bottom: 0;
+}
+dl {
+ margin: 0 1.625em;
+}
+dt {
+ font-weight: bold;
+}
+dd {
+ margin-bottom: 1.625em;
+}
+strong {
+ font-weight: bold;
+}
+cite, em, i {
+ font-style: italic;
+}
+blockquote {
+ font-style: italic;
+ font-weight: normal;
+ margin: 0;
+}
+blockquote em, blockquote i, blockquote cite {
+ font-style: normal;
+}
+blockquote cite {
+ color: #666;
+ font-size: 0.800em;
+ font-weight: 300;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+pre {
+ background: #f4f4f4;
+ font: 1em "Courier 10 Pitch", Courier, monospace;
+ line-height: 1.5;
+ margin-bottom: 1.625em;
+ overflow: auto;
+ padding: 0.75em 1.625em;
+}
+code, kbd {
+ font: 1em Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace;
+}
+abbr, acronym, dfn {
+ border-bottom: 1px dotted #666;
+ cursor: help;
+}
+address {
+ display: block;
+ margin: 0 0 1.625em;
+}
+ins {
+ background: #fff9c0;
+ text-decoration: none;
+}
+sup,
+sub {
+ font-size: 0.667em;
+ height: 0;
+ line-height: 1;
+ position: relative;
+ vertical-align: baseline;
+}
+sup {
+ bottom: 1ex;
+}
+sub {
+ top: .5ex;
+}
+
+/* Forms */
+input[type=text],
+input[type=email],
+input[type=password],
+textarea {
+ background: #fafafa;
+ -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,0.1);
+ -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.1);
+ box-shadow: inset 0 1px 1px rgba(0,0,0,0.1);
+ border: 1px solid #ddd;
+ color: #888;
+}
+input[type=text]:focus,
+input[type=email]:focus,
+textarea:focus {
+ color: #373737;
+}
+textarea {
+ padding-left: 3px;
+ width: 98%;
+}
+input[type=text],
+input[type=email] {
+ padding: 3px;
+}
+input#s {
+ border-radius: 2px;
+ height: 1.692em;
+ line-height: 1.2;
+ padding: 0.4em 0.6em 0.29em;
+}
+input#s:focus {
+ padding-bottom: 0.3em;
+}
+input#searchsubmit {
+ display: none;
+}
+input#s:focus,
+input[type=text]:focus,
+input[type=email]:focus,
+textarea:focus {
+ font-size: 1.231em;
+}
+
+/* Links */
+a {
+ color: #278dbc;
+ text-decoration: none;
+}
+a:hover,
+.entry-title a:hover,
+.entry-meta .edit-link a:hover,
+.commentlist .edit-link a:hover,
+.entry-meta .comments-link a:hover {
+ color: #7dcae7;
+}
+/* Assistive text */
+.assistive-text {
+ clip: rect(1px, 1px, 1px, 1px);
+ position: absolute !important;
+ visibility: hidden;
+}
+
+
+/* =Structure
+----------------------------------------------- */
+
+#page {
+ margin: 0 auto;
+ padding: 2.5%;
+}
+#branding {
+ margin: 0.6em auto 0;
+ padding: 2.5% 2.5% 1.5%;
+}
+
+#primary,
+#secondary {
+ margin: 0 auto;
+ width: auto;
+}
+#secondary {
+ margin-top: 0.8em;
+}
+
+/* Alignment */
+.aligncenter,
+.alignleft,
+.alignright {
+ clear: both;
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* Make sure embeds and iframes scale on smaller screens */
+embed,
+iframe,
+object {
+ width: auto;
+}
+.jetpack-video-wrapper {
+ margin-bottom: 1.0em;
+}
+
+/* Make sure the WordPress Video Shortcode scales on smaller screens */
+video {
+ height: 100% !important;
+ max-width: 100% !important;
+ width: 100% !important;
+}
+.wp-video {
+ width: 100% !important;
+}
+
+
+/* =Header
+----------------------------------------------- */
+
+#branding .site-branding {
+ margin-bottom: 1.3em;
+}
+#site-title,
+#site-description {
+ clear: none;
+}
+#site-title a {
+ color: #111;
+ font-size: 1.846em;
+ font-weight: bold;
+ line-height: 1.3;
+}
+#site-description {
+ color: #7a7a7a;
+ font-size: 0.923em;
+}
+
+/* Header Image */
+#header-img {
+ text-align: center;
+}
+header img {
+ max-width: 100%;
+ height: auto;
+}
+
+
+/* =Navigation and Search Form
+----------------------------------------------- */
+
+.menu-search {
+ background: #1e8cbe;
+ -webkit-box-shadow: inset 0 -1px rgba(0, 86, 132, 0.8), 0 1px 3px rgba(0, 86, 132, 0.4);
+ -moz-box-shadow: inset 0 -1px 0 rgba(0, 86, 132, 0.8), 0 1px 3px rgba(0, 86, 132, 0.4);
+ box-shadow: inset 0 -1px 0 rgba(0, 86, 132, 0.8), 0 1px 3px rgba(0, 86, 132, 0.4);
+ clear: both;
+ height: 46px;
+ width: 100%;
+}
+.menu-search:after {
+ clear: both;
+ content: "";
+ display: block;
+}
+.menu-search,
+#access,
+.menu-toggle,
+#access .menu-label {
+ height: 46px;
+}
+
+/* Small menu */
+#access {
+ float: left;
+ width: 60%;
+}
+.search-form {
+ float: right;
+ text-align: right;
+ width: 39%;
+}
+.menu-toggle {
+ cursor: pointer;
+}
+#access h3.toggled-on {
+ opacity: 0.8;
+}
+#access ul.nav-menu {
+ background: #fff;
+ -webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.15), 0 3px 8px rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: 0 0 2px rgba(0, 0, 0, 0.15), 0 3px 8px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 0 2px rgba(0, 0, 0, 0.15), 0 3px 8px rgba(0, 0, 0, 0.1);
+ display: none;
+ position: absolute;
+ left: 0.5em;
+ top: 3em;
+ width: 100%;
+ z-index: 99999;
+}
+.admin-bar #access ul.nav-menu {
+ top: 6.6em;
+}
+.main-small-navigation .menu {
+ background: #f9f9f9;
+ border: 1px solid #e9e9e9;
+ position: absolute;
+ width: 100%;
+}
+#access ul.nav-menu:before {
+ color: #fff;
+ content: '\f500';
+ display: inline-block;
+ font: 0.9em/1 'Genericons';
+ left: 49px;
+ position: absolute;
+ top: -11px;
+ }
+#access ul li {
+ border-bottom: 1px solid rgba( 0, 0, 0, 0.1 );
+ padding: 1em 0.8em;
+}
+#access ul li:last-of-type {
+ border: none;
+}
+#access ul ul li,
+#access ul ul ul li {
+ border: none;
+ padding-bottom: 0;
+}
+#access a {
+ display: block;
+ font-size: 1em;
+}
+#access ul {
+ display: none;
+ list-style: none;
+ margin: 0;
+ padding: 0.5em 0;
+}
+#access ul ul {
+ display: block;
+}
+#access .sub-menu {
+ margin: 0 0 0 15px;
+}
+#access .menu-toggle {
+ clear: none;
+ color: #fff;
+ font-size: 1.077em;
+ line-height: 2.5;
+ padding: 0.3em 0 0 0.8em;
+}
+#access .menu-toggle:after {
+ content: '\f502';
+ cursor: pointer;
+ display: inline-block;
+ font: 0.7em/1 'Genericons';
+ margin-left: 0.8em;
+ margin-top: 1.7em;
+ position: absolute;
+}
+#access .toggled-on:after {
+ opacity: 0.8;
+}
+#access ul.nav-menu.toggled-on {
+ display: inline-block;
+}
+.search-form #s {
+ background: #006d9d;
+ border: 1px solid #00587f;
+ border-width: 0 0 0 1px;
+ border-radius: 0;
+ color: rgba( 255, 255, 255, 0.6 );
+ font-size: 1em;
+ height: 30px;
+ margin-right: 0;
+ padding: 0.6em;
+ width: 80%;
+}
+
+
+/* =Content
+----------------------------------------------- */
+
+.page-title {
+ color: #666;
+ font-size: 0.769em;
+ font-weight: 300;
+ letter-spacing: 0.1em;
+ line-height: 2.6;
+ margin-bottom: 1.2em;
+ text-transform: uppercase;
+}
+.page-title a {
+ font-size: 0.923em;
+ font-weight: bold;
+ letter-spacing: 0;
+ text-transform: none;
+}
+.hentry,
+.no-results {
+ clear: both;
+ margin: 0 0 1.5em;
+ padding: 0 0 2em;
+ position: relative;
+}
+.hentry {
+ border-bottom: 1px solid #ececec;
+}
+.hentry:last-child,
+.no-results,
+body.singular .hentry {
+ border-bottom: none;
+}
+.blog .sticky .entry-header .entry-meta {
+ clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
+ clip: rect(1px, 1px, 1px, 1px);
+ position: absolute !important;
+}
+.entry-title {
+ clear: both;
+ font-size: 1.538em;
+ line-height: 1.260;
+ word-wrap: break-word;
+}
+.featured-post .entry-title {
+ font-size: 1.077em;
+}
+.entry-title,
+.entry-title a {
+ color: #333;
+ text-decoration: none;
+}
+.entry-meta {
+ color: #666;
+ clear: both;
+ font-size: 0.923em;
+ font-weight: 300;
+ line-height: 1.385;
+ overflow: hidden;
+ padding: 0 0 0.6em 0;
+}
+.entry-meta .entry-date,
+.entry-meta .entry-gallery {
+ display: block;
+ float: left;
+}
+.entry-meta .author-link {
+ display: block;
+ margin-top: 0.7em;
+}
+.entry-meta .comments-link {
+ display: block;
+ float: right;
+}
+.entry-meta .edit-link a {
+ margin-right: .5em;
+}
+.entry-meta .edit-link a,
+.commentlist .edit-link a,
+.entry-meta .comments-link a,
+a.comment-reply-link {
+ color: #464646;
+}
+.entry-meta .edit-link a {
+ float: right;
+ text-decoration: none;
+ padding: 0 0.615em;
+}
+.entry-meta .comments-link a:before,
+.entry-meta .edit-link a:before {
+ display: inline-block;
+ -webkit-font-smoothing: antialiased;
+ font: normal 18px/1 'Genericons';
+ margin-right: 0.1em;
+ vertical-align: top;
+}
+.entry-meta .comments-link a:before {
+ content: '\f300';
+ font-family: Genericons;
+}
+.entry-meta .edit-link a:before,
+.commentlist .edit-link a:before {
+ content: '\f411';
+ font-family: Genericons;
+}
+.commentlist .edit-link a:before,
+.entry-meta .comments-link a:before,
+.entry-meta .edit-link a:before,
+a.comment-reply-link:before {
+ color: #7bcbe4;
+}
+.entry-content {
+ font-size: 1em;
+ line-height: 1.538;
+ margin: 1.286em 0;
+ padding: 0;
+}
+.entry-content h1,
+.entry-content h2,
+.comment-content h1,
+.comment-content h2,
+.entry-content h3,
+.comment-content h3 {
+ color: #000;
+ margin: 0 0 .8125em;
+}
+.entry-content h1,
+.comment-content h1 {
+ font-size: 1.5em;
+ line-height: 2.9;
+}
+.entry-content h2,
+.comment-content h2 {
+ font-size: 1.4em;
+ line-height: 2.6;
+}
+.entry-content h3,
+.comment-content h3 {
+ font-size: 1.3em;
+ line-height: 2.3;
+}
+.entry-content table,
+.comment-content table {
+ border-bottom: 1px solid #ddd;
+ margin: 0 0 1.625em;
+ width: 100%;
+}
+.entry-content th,
+.comment-content th {
+ color: #666;
+ font-size: 0.769em;
+ font-weight: 500;
+ letter-spacing: 0.1em;
+ line-height: 2.6;
+ text-transform: uppercase;
+}
+.entry-content td,
+.comment-content td {
+ border-top: 1px solid #ddd;
+ padding: 0.600em 1em 0.600em 0;
+}
+.entry-content #s {
+ width: 75%;
+}
+.comment-content ul,
+.comment-content ol {
+ margin-bottom: 1.625em;
+}
+.comment-content ul ul,
+.comment-content ol ol,
+.comment-content ul ol,
+.comment-content ol ul {
+ margin-bottom: 0;
+}
+dl.gallery-item {
+ margin: 0;
+}
+.page-link {
+ clear: both;
+ display: block;
+ margin: 0 0 1em;
+}
+.page-link a {
+ background: #278dbc;
+ color: #fff;
+ margin: 0;
+ padding: 0.1em 0.231em;
+ text-decoration: none;
+}
+.page-link span {
+ margin-right: 0.462em;
+}
+
+/* Images */
+.entry-content img,
+.comment-content img,
+.widget img {
+ height: auto;
+ max-width: 100% !important; /* Fluid images for posts, comments, and widgets */
+}
+#content .gallery-columns-3 .gallery-item img,
+#content .gallery-columns-4 .gallery-item img,
+#content .gallery-columns-2 .gallery-item img {
+ width: 100%;
+ height: auto;
+}
+img[class*="align"],
+img[class*="wp-image-"],
+img[class*="attachment-"] {
+ height: auto; /* Make sure images with WordPress-added height and width attributes are scaled correctly */
+}
+img.size-full,
+img.size-large {
+ max-width: 100%;
+ width: auto; /* Prevent stretching of full-size and large-size images with height and width attributes in IE8 */
+ height: auto; /* Make sure images with WordPress-added height and width attributes are scaled correctly */
+}
+img.size-full,
+img.size-large,
+img.size-medium {
+ display: block;
+ margin: 0 auto;
+}
+.entry-content img.wp-smiley {
+ border: none;
+ margin-bottom: 0;
+ margin-top: 0;
+ padding: 0;
+}
+img.alignleft,
+img.alignright,
+img.aligncenter {
+ margin-bottom: 1.625em;
+}
+p img,
+.wp-caption {
+ margin-top: 0.4em;
+}
+.wp-caption {
+ max-width: 96%;
+}
+.wp-caption img {
+ display: block;
+ margin: 0 auto;
+ max-width: 98%;
+}
+.wp-caption .wp-caption-text,
+.gallery-caption {
+ color: #999;
+ font-size: 0.923em;
+}
+.wp-caption .wp-caption-text {
+ margin-bottom: 0.6em;
+ padding: 0.833em 0 0.417em 0;
+ position: relative;
+}
+#content .gallery {
+ margin: 0 auto 1.625em;
+}
+#content .gallery a img {
+ border: none;
+}
+#content .gallery-columns-3 .gallery-item {
+ width: 31%;
+ padding-right: 2%;
+}
+#content .gallery-columns-4 .gallery-item {
+ width: 23%;
+ padding-right: 2%;
+}
+#content .gallery-columns-2 .gallery-item {
+ width: 45%;
+ padding-right: 4%;
+}
+
+
+/* Make sure embeds and iframes fit their containers */
+embed,
+iframe,
+object {
+ max-width: 100%;
+}
+
+/* Password Protected Posts */
+.post-password-required .entry-header .comments-link {
+ margin: 1.625em 0 0;
+}
+.post-password-required input[type=password] {
+ margin: 0.8125em 0;
+}
+.post-password-required input[type=password]:focus {
+ background: #f7f7f7;
+}
+
+
+/*
+Post Formats Headings
+To hide the headings, display: none the ".entry-header .entry-format" selector,
+and remove the padding rules below.
+*/
+.entry-header .entry-format {
+ color: #666;
+ font-size: 0.7em;
+ font-weight: 300;
+ letter-spacing: 0.1em;
+ line-height: 2em;
+ position: absolute;
+ text-transform: uppercase;
+ top: -5px;
+}
+.entry-header .entry-heading .entry-title {
+ padding-top: 0.8em;
+}
+.entry-header .entry-heading {
+ position: relative;
+}
+.entry-thumbnail {
+ margin-bottom: 0.7em;
+ text-align: center;
+}
+
+/* Singular content styles for Posts and Pages */
+.singular .hentry {
+ padding: 1.625em 0 0;
+ position: relative;
+}
+.page .hentry {
+ padding-bottom: .7em;
+}
+.singular .entry-meta .edit-link a {
+ bottom: auto;
+ left: 0;
+ position: absolute;
+ right: auto;
+ top: 40px;
+}
+.single-format-gallery .hentry {
+ margin-bottom: 0;
+}
+.singular #author-info {
+ margin: 2.2em -8.8% 0;
+ padding: 1.538em 8.8%;
+}
+
+
+/* =Gallery Posts
+----------------------------------------------- */
+
+#content .gallery {
+ margin-bottom: 0;
+}
+.format-gallery img {
+ margin: 0;
+}
+.format-gallery .gallery-large {
+ line-height: 1.2em;
+ margin: 0;
+ width: 100%;
+}
+.format-gallery .gallery-thumbs-2,
+ .format-gallery .gallery-thumbs-3 {
+ overflow: hidden;
+ width: 100%;
+}
+.format-gallery .gallery-thumbs-2 img {
+ margin-right: 0.3%;
+ max-width: 48%;
+}
+.format-gallery .gallery-thumbs-3 img {
+ float: left;
+ margin-left: 0.2%;
+ max-width: 33%;
+}
+.format-gallery .gallery-large,
+.format-gallery .gallery-thumbs-2 img,
+ .format-gallery .gallery-thumbs-3 img {
+ display: inline-block;
+}
+.format-gallery .gallery-thumbs-3 .gallery-thumb-1 img {
+ margin: 0;
+}
+.gallery-info {
+ margin-top: 1.3em;
+}
+
+/* =Quote Posts
+----------------------------------------------- */
+
+.format-quote blockquote {
+ color: #555;
+ font-size: 1.308em;
+ margin: 0;
+}
+
+/* =error404
+----------------------------------------------- */
+
+.error404 #main #searchform {
+ background: #f9f9f9;
+ border: 1px solid #ddd;
+ border-width: 1px 0;
+ margin: 0 -8.9% 1.625em;
+ overflow: hidden;
+ padding: 1.625em 8.9%;
+}
+.error404 #main #s {
+ width: 95%;
+}
+.error404 .widgettitle {
+ font-size: 0.769em;
+ letter-spacing: 0.1em;
+ line-height: 2.6em;
+ text-transform: uppercase;
+}
+
+
+/* =Attachments
+----------------------------------------------- */
+
+.image-attachment div.attachment {
+ background: #f9f9f9;
+ border: 1px solid #ddd;
+ border-width: 1px 0;
+ margin: 0 -8.9% 1.625em;
+ overflow: hidden;
+ padding: 1.625em 1.625em 0;
+ text-align: center;
+}
+.image-attachment div.attachment img {
+ display: block;
+ height: auto;
+ margin: 0 auto 1.625em;
+ max-width: 100%;
+}
+.image-attachment div.attachment a img {
+ border-color: #f9f9f9;
+}
+.image-attachment div.attachment a:focus img,
+.image-attachment div.attachment a:hover img,
+.image-attachment div.attachment a:active img {
+ border-color: #ddd;
+ background: #fff;
+}
+
+
+/* =Navigation
+-------------------------------------------------------------- */
+
+#content nav {
+ clear: both;
+}
+#nav-below,
+#nav-single {
+ margin: 0 auto 1.9em;
+ overflow: hidden;
+ width: 100%;
+}
+.nav-previous {
+ float: left;
+ width: 48%;
+}
+.nav-next {
+ float: right;
+ width: 46%;
+}
+#nav-single {
+ display: block;
+ position: static;
+}
+#nav-single .nav-previous {
+ margin-left: 0;
+ width: 50%;
+}
+#nav-single .nav-next {
+ margin-right: 0;
+ width: 49%;
+}
+.nav-previous a,
+.nav-next a {
+ background: #278dbc;
+ color: #fff;
+ display: block;
+ font-size: 1.231em;
+ padding: 1em 0;
+ text-align: center;
+ width: 100%;
+}
+#content nav .meta-nav {
+ font-weight: normal;
+}
+#jp-post-flair {
+ margin: 1em auto !important;
+}
+
+
+
+/* =Widget Area & Widgets
+----------------------------------------------- */
+
+.widget-area {
+ background: #f9f9f9;
+ border-top: 1px solid #ddd;
+ color: #666;
+ font-size: 0.923em;
+ padding: .6em 0.8em;
+ -moz-box-shadow: 0 1px 2px rgba( 0,0,0,0.075 );
+ -webkit-box-shadow: 0 1px 2px rgba( 0,0,0,0.075 );
+ box-shadow: 0 1px 2px rgba( 0,0,0,0.075 );
+}
+.widget {
+ border-bottom: 1px solid #ddd;
+ clear: both;
+ margin: 0;
+ overflow: hidden;
+ padding: 1em 0;
+}
+.widget:last-of-type {
+ border: 0;
+}
+.widget-title {
+ color: #666;
+ font-size: 1.2em;
+ font-weight: bold;
+ line-height: 2em;
+ margin-bottom: 0.5em;
+}
+.widget-title a {
+ color: #666;
+}
+.widget ul {
+ list-style: none;
+ margin-bottom: 0;
+ margin-left: 0;
+}
+.widget ul ul {
+ margin-left: 1.5em;
+}
+.widget ul li {
+ color: #777;
+}
+.widget a {
+ font-weight: normal;
+ text-decoration: none;
+}
+.widget a:hover,
+.widget a:focus,
+.widget a:active {
+ text-decoration: underline;
+}
+
+/* Search Widget */
+.widget_search #s {
+ width: 77%;
+}
+.widget_search #searchsubmit {
+ background: #ddd;
+ border: 1px solid #ccc;
+ -webkit-box-shadow: inset 0px -1px 1px rgba(0, 0, 0, 0.09);
+ -moz-box-shadow: inset 0px -1px 1px rgba(0, 0, 0, 0.09);
+ box-shadow: inset 0px -1px 1px rgba(0, 0, 0, 0.09);
+ color: #888;
+ line-height: 2.083em;
+ position: relative;
+ top: -2px;
+}
+.widget_search #searchsubmit:active {
+ background: #278dbc;
+ border-color: #0861a5;
+ -webkit-box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.1);
+ box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.1);
+ color: #bfddf3;
+}
+
+/* Twitter */
+.tweets {
+ margin-left: 0;
+}
+.widget_twitter li {
+ list-style-type: none;
+ margin-bottom: 1.167em;
+}
+.widget_twitter .timesince {
+ font-size: 0.917em;
+ font-weight: normal;
+ text-align: right;
+}
+
+/* RSS-Related Widgets */
+.widget_rss img {
+ display: inline-block;
+ margin: 0;
+ vertical-align: middle;
+}
+.widget_rss .rss-date {
+ font-size: 90%;
+}
+.widget_rss_links img,
+.widget_rss_links a:hover img,
+.widget_rss_links a:focus img,
+.widget_rss_links a:active img {
+ background: transparent;
+ border: none;
+ padding: 0;
+}
+
+/* Calendar Widget */
+.widget_calendar #wp-calendar {
+ color: #555;
+ width: 95%;
+ text-align: center;
+}
+.widget_calendar #wp-calendar caption,
+.widget_calendar #wp-calendar td,
+.widget_calendar #wp-calendar th {
+ text-align: center;
+}
+.widget_calendar #wp-calendar caption {
+ font-size: 11px;
+ font-weight: 500;
+ padding: 5px 0 3px 0;
+ text-transform: uppercase;
+}
+.widget_calendar #wp-calendar th {
+ background: #f4f4f4;
+ border-top: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ font-weight: bold;
+}
+.widget_calendar #wp-calendar tfoot td {
+ background: #f4f4f4;
+ border-top: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+}
+
+/* Recent Comments */
+.widget_recent_comments td.recentcommentstexttop,
+.widget_recent_comments td.recentcommentstextend {
+ vertical-align: top;
+}
+
+/* Authors Widget */
+.widget_authors ul {
+ margin-left: 0;
+}
+.widget_authors li {
+ background: none !important;
+ overflow: hidden;
+}
+.widget_authors ul ul li {
+ overflow: hidden;
+}
+.widget_authors img {
+ float: left;
+ padding-right: 0.833em;
+ vertical-align: text-top;
+}
+
+/*Flickr Widget */
+.widget_flickr #flickr_badge_wrapper {
+ background-color: transparent;
+ border: none;
+}
+#flickr_badge_uber_wrapper a:hover,
+#flickr_badge_uber_wrapper a:link,
+#flickr_badge_uber_wrapper a:active,
+#flickr_badge_uber_wrapper a:visited {
+ color: #278dbc !important;
+}
+
+
+/* =Comments
+----------------------------------------------- */
+
+#comments {
+ margin-top: 1.5em;
+}
+#comments-title {
+ color: #000;
+ font-size: 1.154em;
+ font-weight: bold;
+ line-height: 1em;
+ padding: 1em 0;
+}
+#comment-nav-below {
+ overflow: hidden;
+}
+.nopassword,
+.nocomments {
+ color: #aaa;
+ font-size: 1.846em;
+ font-weight: 100;
+ margin: 2em 0;
+ text-align: center;
+}
+.commentlist {
+ list-style: none;
+ margin: 2em auto;
+ width: 100%;
+}
+.commentlist > li.comment {
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-width: 1px 0 0;
+ clear: both;
+ margin: 0 -2.5% 0.5em;
+ overflow: hidden;
+ padding: .8em;
+ position: relative;
+}
+.commentlist > li.comment,
+.commentlist .pingback {
+ width: auto;
+}
+
+/* Reblogs */
+.commentlist > li.reblog {
+ border: 1px solid #eee;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ margin: 0 0 1.625em;
+ padding: 1.625em;
+ position: relative;
+}
+.commentlist .reblog .comment-meta {
+ display: none;
+}
+.commentlist .reblog p:first-child {
+ color: #999;
+ font-size: 1em;
+}
+.commentlist .reblog p:first-child a {
+ font-weight: bold;
+}
+.commentlist .pingback {
+ border-top: 1px solid rgba( 0, 0, 0, 0.1 );
+ margin: 0 -2.5% 0.5em;
+}
+.commentlist .pingback p {
+ color: #c0c0c0;
+ margin-bottom: 0;
+ padding: 0.8em;
+}
+.commentlist .children {
+ list-style: none;
+ margin: 0;
+}
+.commentlist .children li.comment {
+ background: #f9f9f9;
+ border-top: 1px solid rgba( 0, 0, 0, 0.1 );
+ clear: both;
+ margin: 1.625em 0 0;
+ overflow: hidden;
+ padding: 1.625em 1.625em 0.5em;
+ position: relative;
+}
+.comment-meta .fn {
+ font-style: normal;
+}
+.comment-meta,
+.comment-content {
+ margin-left: 4em;
+}
+.comment-meta {
+ font-size: 0.923em;
+}
+.comment-content {
+ margin-top: 1em;
+}
+.commentlist .children li.comment .comment-meta {
+ line-height: 1.625;
+ margin-left: 3.462em;
+}
+.commentlist .children li.comment .comment-content {
+ margin: 1em 0 0 3.1em;
+}
+.comment-meta a:focus,
+.comment-meta a:active,
+.comment-meta a:hover {
+}
+.commentlist .avatar {
+ background: transparent;
+ display: block;
+ padding: 0;
+ position: absolute;
+ left: 13px;
+ top: 13px;
+}
+.commentlist .children .avatar {
+ background: none;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ left: 2.2em;
+ padding: 0;
+ position: absolute;
+ top: 2.2em;
+}
+.comment-actions {
+ float: right;
+ font-weight: 300;
+ margin-bottom: 1em;
+}
+a.comment-reply-link:before {
+ content: '\f412';
+ display: inline-block;
+ -webkit-font-smoothing: antialiased;
+ font: normal 13px/1 'Genericons';
+ margin: 0.2em 0.1em 0 0;
+ vertical-align: top;
+}
+a.comment-reply-link {
+ display: inline-block;
+ font-size: 0.923em;
+ padding: 0 0.615em;
+ text-decoration: none;
+}
+a.comment-reply-link > span {
+ display: inline-block;
+ position: relative;
+ top: -1px;
+}
+
+/* Comment Form */
+#respond {
+ margin: 0 auto 1.625em;
+ width: auto;
+}
+#respond input[type="text"],
+#respond textarea {
+ background: #fff;
+ position: relative;
+ padding: 0.615em;
+}
+#respond .comment-form-author,
+#respond .comment-form-email,
+#respond .comment-form-url,
+#respond .comment-form-comment {
+ position: relative;
+}
+#respond .comment-form-author label,
+#respond .comment-form-email label,
+#respond .comment-form-url label,
+#respond .comment-form-comment label {
+ display: inline-block;
+ font-size: 1.077em;
+ padding: 0.154em 0 0;
+ position: relative;
+}
+#respond textarea {
+ resize: vertical;
+ width: 95%;
+}
+#respond .comment-form-author .required,
+#respond .comment-form-email .required {
+ color: #bd3500;
+ font-size: 1.692em;
+ font-weight: bold;
+ left: 95%;
+ position: absolute;
+ top: 45px;
+ z-index: 1;
+}
+#respond .comment-notes,
+#respond .logged-in-as {
+ font-size: 0.8em;
+ color: #666;
+}
+#respond .logged-in-as {
+ margin-top: 1em;
+}
+#respond p {
+ margin: 0.769em 0 0;
+}
+#respond .form-submit {
+ margin: 0;
+}
+#respond input#submit {
+ background-color: #2ea2cc;
+ background-image: -moz-linear-gradient(top, #278dbc 0%, #0074a2 100%);
+ background-image: -webkit-linear-gradient(top, #278dbc 0%,#0074a2 100%);
+ background-image: -ms-linear-gradient(top, #278dbc 0%,#0074a2 100%);
+ background-image: -o-linear-gradient(top, #278dbc 0%,#0074a2 100%);
+ background-image: linear-gradient(top, #278dbc 0%,#0074a2 100%);
+
+ -moz-box-shadow: inset 0 1px 0 rgba(120,200,230,0.5);
+ -webkit-box-shadow: inset 0 1px 0 rgba(120,200,230,0.5);
+ -o-box-shadow: inset 0 1px 0 rgba(120,200,230,0.5);
+ -ms-box-shadow: inset 0 1px 0 rgba(120,200,230,0.5);
+ box-shadow: inset 0 1px 0 rgba(120,200,230,0.5);
+ border: none;
+ -moz-border-radius: 2px 0 0 0;
+ -webkit-border-radius: 2px 0 0 0;
+ -o-border-radius: 2px 0 0 0;
+ -ms-border-radius: 2px 0 0 0;
+ border-radius: 2px;
+ color: rgba( 255, 255, 255, 0.9 );
+ cursor: pointer;
+ font-size: 1em;
+ margin: 0.462em 0;
+ padding: 1em;
+ left: 30px;
+ text-shadow: 0 -1px 0 rgba(0,116,162,0.8);
+}
+#respond input#submit:hover {
+ color: #fff;
+ text-shadow: 0 -1px 0 rgb(0,116,162);
+
+ background-image: -moz-linear-gradient(top, #2ea2cc 0%, #0074a2 100%);
+ background-image: -webkit-linear-gradient(top, #2ea2cc 0%,#0074a2 100%);
+ background-image: -o-linear-gradient(top, #2ea2cc 0%,#0074a2 100%);
+ background-image: -ms-linear-gradient(top, #2ea2cc 0%,#0074a2 100%);
+ background-image: linear-gradient(top, #2ea2cc 0%,#0074a2 100%);
+
+ -moz-box-shadow: inset 0 1px 0 #2ea2cc, inset 0 2px 0 rgba(120,200,230,0.7);
+ -webkit-box-shadow: inset 0 1px 0 #2ea2cc, inset 0 2px 0 rgba(120,200,230,0.7);
+ -o-box-shadow: inset 0 1px 0 #2ea2cc, inset 0 2px 0 rgba(120,200,230,0.7);
+ -ms-box-shadow: inset 0 1px 0 #2ea2cc, inset 0 2px 0 rgba(120,200,230,0.7);
+ box-shadow: inset 0 1px 0 #2ea2cc, inset 0 2px 0 rgba(120,200,230,0.7);
+}
+#respond input#submit:active {
+ color:rgba(255,255,255,.9);
+ background-image: -moz-linear-gradient(top, #278dbc 0%, #0074a2 100%);
+ background-image: -webkit-linear-gradient(top, #278dbc 0%,#0074a2 100%);
+ background-image: -o-linear-gradient(top, #278dbc 0%,#0074a2 100%);
+ background-image: -ms-linear-gradient(top, #278dbc 0%,#0074a2 100%);
+ background-image: linear-gradient(top, #278dbc 0%,#0074a2 100%);
+
+ -moz-box-shadow: inset 0 1px 5px #005684, inset 0 -1px 0 #278dbc;
+ -webkit-box-shadow: inset 0 1px 5px #005684, inset 0 -1px 0 #278dbc;
+ -o-box-shadow: inset 0 1px 5px #005684, inset 0 -1px 0 #278dbc;
+ -ms-box-shadow: inset 0 1px 5px #005684, inset 0 -1px 0 #278dbc;
+ box-shadow: inset 0 1px 5px #005684, inset 0 -1px 0 #278dbc;
+}
+.commentlist #respond {
+ margin: 1.625em 0 0;
+ width: auto;
+}
+#respond .comment-subscription-form {
+ margin: 6px 0;
+}
+#reply-title {
+ font-size: 1.5em;
+}
+.comment #reply-title {
+ margin-top: 1em;
+}
+#cancel-comment-reply-link {
+ color: #bd3500;
+ display: block;
+ font-size: 0.6em;
+ font-weight: 300;
+ line-height: 2.2;
+ margin-top: 0.4em;
+ text-decoration: none;
+}
+#respond label {
+ line-height: 2.2;
+}
+#respond input[type=text] {
+ display: block;
+ height: 1.846em;
+ width: 95%;
+}
+#respond p {
+ font-size: 0.923em;
+}
+p.comment-form-comment {
+ margin: 0;
+}
+.form-allowed-tags {
+ display: none;
+}
+
+
+/* =Footer
+----------------------------------------------- */
+
+#colophon {
+ background: #f1f1f1;
+ clear: both;
+ margin-bottom: -2em;
+ padding-bottom: 1em;
+}
+
+/* Site Generator Line */
+#site-generator {
+ border-top: 1px solid #ddd;
+ font-size: 0.923em;
+ line-height: 2.2;
+ padding: 2.2em 0.5em;
+ text-align: center;
+}
+#site-generator a {
+ color: #278dbc;
+}
+#site-generator .sep {
+ color: transparent;
+ display: inline-block;
+ height: 16px;
+ line-height: 1.231;
+ margin: 0 0.538em;
+ text-indent: 40px; /* Push the separator just out of the way */
+ width: 3.077em;
+}
+
+
+/* =WP.com
+----------------------------------------------- */
+
+.entry-content .twitter-tweet-rendered {
+ max-width: 100% !important; /* Override the Twitter embed fixed width */
+}
+.video-player {
+ max-width: 100% !important;
+}
+.videopress-placeholder,
+.video-player img {
+ max-width: 100% !important;
+ height: auto !important;
+}
+.syntaxhighlighter {
+ overflow: auto;
+}
+.single #content #wp-likebox,
+.page #content #wp-likebox {
+ display: block;
+}
+#wpl-mustlogin {
+ width: 240px !important;
+ margin-left: -60px !important;
+}
+img.latex {
+ display: inline;
+}
+
+/* WP.com comment form */
+#comments #respond {
+ max-width: 75%;
+ margin: 0 auto 15px;
+}
+.content #comments #respond,
+#comments .commentlist #respond {
+ max-width: 100%;
+}
+#respond textarea {
+ text-indent: 0;
+}
+.singular #content .wpl-likebox {
+ width: 100%;
+}
+#comments #respond {
+ max-width: 100%;
+}
+#wpstats {
+ display: block;
+ margin: -1.8em auto 0;
+}
+#wpstats2 {
+ display: none;
+}
+
+/* Adjust the width of Crowdsignal polls */
+.PDS_Poll .pds-box,
+.CSS_Poll .css-box {
+ width: 99% !important;
+}
+
+/* Infinite Scroll */
+.infinite-wrap {
+ border-top: 1px solid #ececec;
+ padding-top: 1.5em !important;
+}
+.infinite-scroll .hentry:last-of-type,
+.infinite-scroll .hentry.last-before-infinite {
+ margin-bottom: 0;
+}
+.infinite-scroll #content {
+ padding-bottom: 0.1em;
+}
+#infinite-handle span:before {
+ display: none;
+}
+#infinite-handle span {
+ background: #278dbc;
+ border-radius: 2px;
+ border: none;
+ color: #fff;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 400;
+ padding: 6px 26px;
+ text-align: center;
+}
+#infinite-handle span:hover {
+ background: #7dcae7;
+}
+
+/* Remove margins and padding on outer containers for super-tiny screens */
+@media only screen and (min-device-width: 100px) and (max-device-width: 300px) {
+ #wrapper {
+ margin: 0;
+ padding: 0;
+ }
+ #access {
+ padding: 0.385em 0;
+ }
+ #page,
+ .widget-area,
+ #main,
+ #branding {
+ width: 100%;
+ margin: 0;
+ }
+ .widget-area {
+ padding: 0.417em;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/mobile-push.php b/plugins/jetpack/modules/mobile-push.php
new file mode 100644
index 00000000..a421f47d
--- /dev/null
+++ b/plugins/jetpack/modules/mobile-push.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. See notes.php for the new module
+ *
+ * @package Jetpack
+ **/
diff --git a/plugins/jetpack/modules/module-extras.php b/plugins/jetpack/modules/module-extras.php
new file mode 100644
index 00000000..989512dc
--- /dev/null
+++ b/plugins/jetpack/modules/module-extras.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Load module code that is needed even when a module isn't active.
+ * For example, if a module shouldn't be activatable unless certain conditions are met,
+ * the code belongs in this file.
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Features available all the time:
+ * - When in development mode.
+ * - When connected to WordPress.com.
+ */
+$tools = array(
+ // Always loaded, but only registered if theme supports it.
+ 'custom-post-types/comics.php',
+ 'custom-post-types/testimonial.php',
+ 'custom-post-types/nova.php',
+ 'geo-location.php',
+ 'theme-tools.php',
+ 'theme-tools/social-links.php',
+ 'theme-tools/random-redirect.php',
+ 'theme-tools/featured-content.php',
+ 'theme-tools/infinite-scroll.php',
+ 'theme-tools/responsive-videos.php',
+ 'theme-tools/site-logo.php',
+ 'theme-tools/site-breadcrumbs.php',
+ 'theme-tools/social-menu.php',
+ 'theme-tools/content-options.php',
+ // Needed for SEO Tools.
+ 'seo-tools/jetpack-seo-utils.php',
+ 'seo-tools/jetpack-seo-titles.php',
+ 'seo-tools/jetpack-seo-posts.php',
+ 'verification-tools/verification-tools-utils.php',
+ // Needed for VideoPress, so videos keep working in existing posts/pages when the module is deactivated.
+ 'videopress/utility-functions.php',
+ 'videopress/class.videopress-gutenberg.php',
+);
+
+// Some features are only available when connected to WordPress.com.
+$connected_tools = array(
+ 'calypsoify/class.jetpack-calypsoify.php',
+ 'plugin-search.php',
+ 'simple-payments/simple-payments.php',
+ 'woocommerce-analytics/wp-woocommerce-analytics.php',
+ 'wpcom-block-editor/class-jetpack-wpcom-block-editor.php',
+);
+
+// Add connected features to our existing list if the site is currently connected.
+if ( Jetpack::is_active() ) {
+ $tools = array_merge( $tools, $connected_tools );
+}
+
+/**
+ * Filter extra tools (not modules) to include.
+ *
+ * @since 2.4.0
+ * @since 5.4.0 can be used in multisite when Jetpack is not connected to WordPress.com and not in development mode.
+ *
+ * @param array $tools Array of extra tools to include.
+ */
+$jetpack_tools_to_include = apply_filters( 'jetpack_tools_to_include', $tools );
+
+if ( ! empty( $jetpack_tools_to_include ) ) {
+ foreach ( $jetpack_tools_to_include as $tool ) {
+ if ( file_exists( JETPACK__PLUGIN_DIR . '/modules/' . $tool ) ) {
+ require_once JETPACK__PLUGIN_DIR . '/modules/' . $tool;
+ }
+ }
+}
+
+/**
+ * Add the "(Jetpack)" suffix to the widget names
+ *
+ * @param string $widget_name Widget name.
+ */
+function jetpack_widgets_add_suffix( $widget_name ) {
+ return sprintf(
+ /* Translators: Placeholder is the name of a widget. */
+ __( '%s (Jetpack)', 'jetpack' ),
+ $widget_name
+ );
+}
+add_filter( 'jetpack_widget_name', 'jetpack_widgets_add_suffix' );
diff --git a/plugins/jetpack/modules/module-headings.php b/plugins/jetpack/modules/module-headings.php
new file mode 100644
index 00000000..62cc135a
--- /dev/null
+++ b/plugins/jetpack/modules/module-headings.php
@@ -0,0 +1,342 @@
+<?php
+// Do not edit this file. It's generated by jetpack/tools/build-module-headings-translations.php
+
+/**
+ * For a given module, return an array with translated name, description and recommended description.
+ *
+ * @param string $key Module file name without .php
+ *
+ * @return array
+ */
+function jetpack_get_module_i18n( $key ) {
+ static $modules;
+ if ( ! isset( $modules ) ) {
+ $modules = array(
+ 'carousel' => array(
+ 'name' => _x( 'Carousel', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Display images and galleries in a gorgeous, full-screen browsing experience', 'Module Description', 'jetpack' ),
+ ),
+
+ 'comment-likes' => array(
+ 'name' => _x( 'Comment Likes', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Increase visitor engagement by adding a Like button to comments.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'comments' => array(
+ 'name' => _x( 'Comments', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Let readers use WordPress.com, Twitter, Facebook, or Google+ accounts to comment', 'Module Description', 'jetpack' ),
+ ),
+
+ 'contact-form' => array(
+ 'name' => _x( 'Contact Form', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Add a customizable contact form to any post or page using the Jetpack Form Block.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'copy-post' => array(
+ 'name' => _x( 'Copy Post', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Copy an existing post\'s content into a new draft post', 'Module Description', 'jetpack' ),
+ 'recommended description' => _x( 'Copy an existing post\'s content into a new draft post', 'Jumpstart Description', 'jetpack' ),
+ ),
+
+ 'custom-content-types' => array(
+ 'name' => _x( 'Custom content types', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Display different types of content on your site with custom content types.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'custom-css' => array(
+ 'name' => _x( 'Custom CSS', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Adds options for CSS preprocessor use, disabling the theme\'s CSS, or custom image width.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'enhanced-distribution' => array(
+ 'name' => _x( 'Enhanced Distribution', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Increase reach and traffic.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'google-analytics' => array(
+ 'name' => _x( 'Google Analytics', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Set up Google Analytics without touching a line of code.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'gravatar-hovercards' => array(
+ 'name' => _x( 'Gravatar Hovercards', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Enable pop-up business cards over commenters’ Gravatars.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'infinite-scroll' => array(
+ 'name' => _x( 'Infinite Scroll', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Automatically load new content when a visitor scrolls', 'Module Description', 'jetpack' ),
+ ),
+
+ 'json-api' => array(
+ 'name' => _x( 'JSON API', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Allow applications to securely access your content.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'latex' => array(
+ 'name' => _x( 'Beautiful Math', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Use the LaTeX markup language to write mathematical equations and formulas.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'lazy-images' => array(
+ 'name' => _x( 'Lazy Images', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Speed up your site and create a smoother viewing experience by loading images as visitors scroll down the screen, instead of all at once.', 'Module Description', 'jetpack' ),
+ 'recommended description' => _x( 'Lazy-loading images improve your site\'s speed and create a smoother viewing experience. Images will load as visitors scroll down the screen, instead of all at once.', 'Jumpstart Description', 'jetpack' ),
+ ),
+
+ 'likes' => array(
+ 'name' => _x( 'Likes', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Give visitors an easy way to show they appreciate your content.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'markdown' => array(
+ 'name' => _x( 'Markdown', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Write posts or pages in plain-text Markdown syntax', 'Module Description', 'jetpack' ),
+ ),
+
+ 'masterbar' => array(
+ 'name' => _x( 'WordPress.com Toolbar', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Replaces the admin bar with a useful toolbar to quickly manage your site via WordPress.com.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'minileven' => array(
+ 'name' => _x( 'Mobile Theme', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Enable the Jetpack Mobile theme', 'Module Description', 'jetpack' ),
+ ),
+
+ 'monitor' => array(
+ 'name' => _x( 'Monitor', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Jetpack’s downtime monitoring will continuously watch your site, and alert you the moment that downtime is detected.', 'Module Description', 'jetpack' ),
+ 'recommended description' => _x( 'Receive immediate notifications if your site goes down, 24/7.', 'Jumpstart Description', 'jetpack' ),
+ ),
+
+ 'notes' => array(
+ 'name' => _x( 'Notifications', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Receive instant notifications of site comments and likes.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'photon-cdn' => array(
+ 'name' => _x( 'Asset CDN', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Jetpack’s Site Accelerator loads your site faster by optimizing your images and serving your images and static files from our global network of servers.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'photon' => array(
+ 'name' => _x( 'Image CDN', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Mirrors and serves your images from our free and fast image CDN, improving your site’s performance with no additional load on your servers.', 'Module Description', 'jetpack' ),
+ 'recommended description' => _x( 'Mirrors and serves your images from our free and fast image CDN, improving your site’s performance with no additional load on your servers.', 'Jumpstart Description', 'jetpack' ),
+ ),
+
+ 'post-by-email' => array(
+ 'name' => _x( 'Post by email', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Publish posts by sending an email', 'Module Description', 'jetpack' ),
+ ),
+
+ 'protect' => array(
+ 'name' => _x( 'Protect', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Protect yourself from brute force and distributed brute force attacks, which are the most common way for hackers to get into your site.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'publicize' => array(
+ 'name' => _x( 'Publicize', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Publicize makes it easy to share your site’s posts on several social media networks automatically when you publish a new post.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'pwa' => array(
+ 'name' => _x( 'Progressive Web Apps', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Speed up and improve the reliability of your site using the latest in web technology.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'related-posts' => array(
+ 'name' => _x( 'Related posts', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Keep visitors engaged on your blog by highlighting relevant and new content at the bottom of each published post.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'search' => array(
+ 'name' => _x( 'Search', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Enhanced search, powered by Elasticsearch, a powerful replacement for WordPress search.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'seo-tools' => array(
+ 'name' => _x( 'SEO Tools', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Better results on search engines and social media.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'sharedaddy' => array(
+ 'name' => _x( 'Sharing', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Add Twitter, Facebook and Google+ buttons at the bottom of each post, making it easy for visitors to share your content.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'shortcodes' => array(
+ 'name' => _x( 'Shortcode Embeds', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Shortcodes are WordPress-specific markup that let you add media from popular sites. This feature is no longer necessary as the editor now handles media embeds rather gracefully.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'shortlinks' => array(
+ 'name' => _x( 'WP.me Shortlinks', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Generates shorter links so you can have more space to write on social media sites.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'sitemaps' => array(
+ 'name' => _x( 'Sitemaps', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Make it easy for search engines to find your site.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'sso' => array(
+ 'name' => _x( 'Secure Sign On', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Allow users to log into this site using WordPress.com accounts', 'Module Description', 'jetpack' ),
+ 'recommended description' => _x( 'Lets you log in to all your Jetpack-enabled sites with one click using your WordPress.com account.', 'Jumpstart Description', 'jetpack' ),
+ ),
+
+ 'stats' => array(
+ 'name' => _x( 'Site Stats', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Collect valuable traffic stats and insights.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'subscriptions' => array(
+ 'name' => _x( 'Subscriptions', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Allow users to subscribe to your posts and comments and receive notifications via email', 'Module Description', 'jetpack' ),
+ ),
+
+ 'tiled-gallery' => array(
+ 'name' => _x( 'Tiled Galleries', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Display image galleries in a variety of elegant arrangements.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'vaultpress' => array(
+ 'name' => _x( 'Backups and Scanning', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Protect your site with daily or real-time backups and automated virus scanning and threat detection.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'verification-tools' => array(
+ 'name' => _x( 'Site verification', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Establish your site\'s authenticity with external services.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'videopress' => array(
+ 'name' => _x( 'VideoPress', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Save on hosting storage and bandwidth costs by streaming fast, ad-free video from our global network.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'widget-visibility' => array(
+ 'name' => _x( 'Widget Visibility', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Control where widgets appear on your site.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'widgets' => array(
+ 'name' => _x( 'Extra Sidebar Widgets', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Provides additional widgets for use on your site.', 'Module Description', 'jetpack' ),
+ ),
+
+ 'wordads' => array(
+ 'name' => _x( 'Ads', 'Module Name', 'jetpack' ),
+ 'description' => _x( 'Earn income by allowing Jetpack to display high quality ads.', 'Module Description', 'jetpack' ),
+ ),
+ );
+ }
+ return $modules[ $key ];
+}
+/**
+ * For a given module tag, return its translated version.
+ *
+ * @param string $key Module tag as is in each module heading.
+ *
+ * @return string
+ */
+function jetpack_get_module_i18n_tag( $key ) {
+ static $module_tags;
+ if ( ! isset( $module_tags ) ) {
+ $module_tags = array(
+ // Modules with `Other` tag:
+ // - modules/contact-form.php
+ // - modules/notes.php
+ 'Other' =>_x( 'Other', 'Module Tag', 'jetpack' ),
+
+ // Modules with `Photos and Videos` tag:
+ // - modules/carousel.php
+ // - modules/photon-cdn.php
+ // - modules/photon.php
+ // - modules/shortcodes.php
+ // - modules/tiled-gallery.php
+ // - modules/videopress.php
+ 'Photos and Videos' =>_x( 'Photos and Videos', 'Module Tag', 'jetpack' ),
+
+ // Modules with `Social` tag:
+ // - modules/comment-likes.php
+ // - modules/comments.php
+ // - modules/gravatar-hovercards.php
+ // - modules/likes.php
+ // - modules/publicize.php
+ // - modules/seo-tools.php
+ // - modules/sharedaddy.php
+ // - modules/shortcodes.php
+ // - modules/shortlinks.php
+ // - modules/subscriptions.php
+ // - modules/widgets.php
+ 'Social' =>_x( 'Social', 'Module Tag', 'jetpack' ),
+
+ // Modules with `Writing` tag:
+ // - modules/copy-post.php
+ // - modules/custom-content-types.php
+ // - modules/enhanced-distribution.php
+ // - modules/json-api.php
+ // - modules/latex.php
+ // - modules/markdown.php
+ // - modules/post-by-email.php
+ // - modules/shortcodes.php
+ 'Writing' =>_x( 'Writing', 'Module Tag', 'jetpack' ),
+
+ // Modules with `Appearance` tag:
+ // - modules/custom-css.php
+ // - modules/gravatar-hovercards.php
+ // - modules/infinite-scroll.php
+ // - modules/lazy-images.php
+ // - modules/minileven.php
+ // - modules/photon-cdn.php
+ // - modules/photon.php
+ // - modules/seo-tools.php
+ // - modules/shortcodes.php
+ // - modules/widget-visibility.php
+ // - modules/widgets.php
+ // - modules/wordads.php
+ 'Appearance' =>_x( 'Appearance', 'Module Tag', 'jetpack' ),
+
+ // Modules with `Developers` tag:
+ // - modules/json-api.php
+ // - modules/pwa.php
+ // - modules/sso.php
+ 'Developers' =>_x( 'Developers', 'Module Tag', 'jetpack' ),
+
+ // Modules with `Recommended` tag:
+ // - modules/lazy-images.php
+ // - modules/minileven.php
+ // - modules/monitor.php
+ // - modules/photon-cdn.php
+ // - modules/photon.php
+ // - modules/protect.php
+ // - modules/publicize.php
+ // - modules/related-posts.php
+ // - modules/sharedaddy.php
+ // - modules/sitemaps.php
+ // - modules/stats.php
+ 'Recommended' =>_x( 'Recommended', 'Module Tag', 'jetpack' ),
+
+ // Modules with `General` tag:
+ // - modules/masterbar.php
+ 'General' =>_x( 'General', 'Module Tag', 'jetpack' ),
+
+ // Modules with `Mobile` tag:
+ // - modules/minileven.php
+ 'Mobile' =>_x( 'Mobile', 'Module Tag', 'jetpack' ),
+
+ // Modules with `Traffic` tag:
+ // - modules/sitemaps.php
+ // - modules/wordads.php
+ 'Traffic' =>_x( 'Traffic', 'Module Tag', 'jetpack' ),
+
+ // Modules with `Site Stats` tag:
+ // - modules/stats.php
+ 'Site Stats' =>_x( 'Site Stats', 'Module Tag', 'jetpack' ),
+ );
+ }
+ return ! empty( $module_tags[ $key ] ) ? $module_tags[ $key ] : '';
+}
diff --git a/plugins/jetpack/modules/module-info.php b/plugins/jetpack/modules/module-info.php
new file mode 100644
index 00000000..f13964de
--- /dev/null
+++ b/plugins/jetpack/modules/module-info.php
@@ -0,0 +1,914 @@
+<?php
+/**
+ * "Learn More" information blocks for all modules live in this file.
+ *
+ * Each module must include 2 functions:
+ * - The first one creates a button where users can find more information about the module.
+ * It is hooked into `jetpack_learn_more_button_ . $module`
+ * - The second creates a information block.
+ * It is hooked into `jetpack_module_more_info_ . $module`
+ *
+ * @package Jetpack
+ */
+
+/**
+ * VaultPress (stub) support link.
+ */
+function vaultpress_jetpack_load_more_link() {
+ echo 'https://help.vaultpress.com/get-to-know/';
+}
+add_filter( 'jetpack_learn_more_button_vaultpress', 'vaultpress_jetpack_load_more_link' );
+
+/**
+ * VaultPress (stub) description.
+ */
+function vaultpress_jetpack_more_info() {
+ esc_html_e(
+ 'We keep a daily or real-time backup of your site so that when mistakes or accidents occur, restoring your
+ site to any location takes a matter of minutes. Your site’s files are regularly scanned for unauthorized or
+ suspicious modifications that could compromise your security and data. In many cases, we can fix them
+ automatically (and will notify you). When we can’t, we provide you with expert support.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_vaultpress', 'vaultpress_jetpack_more_info' );
+
+/**
+ * Gravatar Hovercards support link.
+ */
+function grofiles_load_more_link() {
+ echo 'https://jetpack.com/support/gravatar-hovercards/';
+}
+add_filter( 'jetpack_learn_more_button_gravatar-hovercards', 'grofiles_load_more_link' );
+
+/**
+ * Gravatar Hovercards description.
+ */
+function grofiles_more_info() {
+ esc_html_e(
+ 'Enhance plain Gravatar images with information about a person (including a name,
+ bio, pictures, and contact info) when they leave a comment on one of your posts.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_gravatar-hovercards', 'grofiles_more_info' );
+
+/**
+ * Shortcodes support link.
+ */
+function jetpack_shortcodes_load_more_link() {
+ echo 'https://jetpack.com/support/shortcode-embeds/';
+}
+add_filter( 'jetpack_learn_more_button_shortcodes', 'jetpack_shortcodes_load_more_link' );
+
+/**
+ * Shortcodes description.
+ */
+function jetpack_shortcodes_more_info() {
+ esc_html_e(
+ 'Easily and safely embed media from YouTube, Facebook, Flickr, Vimeo, Instagram,
+ Google Maps, SlideShare, Vine, SoundCloud, and more. Just enter the appropriate shortcode directly into the
+ editor and click “Publish.”',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_shortcodes', 'jetpack_shortcodes_more_info' );
+
+/**
+ * Shortlinks support link.
+ */
+function wpme_load_more_link() {
+ echo 'http://wp.me/p1moTy-DL';
+}
+add_filter( 'jetpack_learn_more_button_shortlinks', 'wpme_load_more_link' );
+
+/**
+ * Shortlinks description
+ */
+function wpme_more_info() {
+ esc_html_e(
+ 'Grab short and simple links to your posts and pages using the compact wp.me domain name. Perfect
+ for use on Twitter, Facebook, and in text messages where every character counts.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_shortlinks', 'wpme_more_info' );
+
+/**
+ * Site Stats support link.
+ */
+function stats_load_more_link() {
+ echo 'https://jetpack.com/support/wordpress-com-stats/';
+}
+add_filter( 'jetpack_learn_more_button_stats', 'stats_load_more_link' );
+
+/**
+ * Site Stats description.
+ */
+function stats_more_info() {
+ esc_html_e(
+ 'Simple and concise statistics about your traffic. Jetpack collects data about pageviews, likes, comments,
+ locations, and top posts. View them in your dashboard or on WordPress.com.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_stats', 'stats_more_info' );
+
+/**
+ * Publicize support link.
+ */
+function publicize_load_more_link() {
+ echo 'https://jetpack.com/support/publicize/';
+}
+add_filter( 'jetpack_learn_more_button_publicize', 'publicize_load_more_link' );
+
+/**
+ * Publicize description.
+ */
+function publicize_more_info() {
+ esc_html_e(
+ 'Automatically share and promote newly published posts to Facebook, Twitter, Tumblr,
+ and LinkedIn. You can add connections for yourself or for all users on your site.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_publicize', 'publicize_more_info' );
+
+/**
+ * Notifications
+ */
+function notes_load_more_link() {
+ echo 'https://jetpack.com/support/notifications/';
+}
+add_filter( 'jetpack_learn_more_button_notes', 'notes_load_more_link' );
+
+/**
+ * Notifications description.
+ */
+function notes_more_info() {
+ esc_html_e(
+ 'You will receive instant notifications in your dashboard or your mobile device when somebody comments
+ on any of your sites. Reply directly wherever you are to keep the conversation going.',
+ 'jetpack'
+ );
+}
+add_filter( 'jetpack_module_more_info_notes', 'notes_more_info' );
+
+/**
+ * LaTeX support link.
+ */
+function latex_load_more_link() {
+ echo 'https://jetpack.com/support/beautiful-math-with-latex/';
+}
+add_filter( 'jetpack_learn_more_button_latex', 'latex_load_more_link' );
+
+/**
+ * LaTeX description.
+ */
+function latex_more_info() {
+ esc_html_e(
+ 'LaTeX is a powerful markup language for writing complex mathematical equations and formulas.
+ Jetpack combines the power of LaTeX and the simplicity of WordPress to give you the ultimate
+ in math blogging platforms. Use $latex your latex code here$ or [latex]your latex code here[/latex]
+ to include in your posts and comments. Enjoy all sorts of options and embrace your inner nerd.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_latex', 'latex_more_info' );
+
+/**
+ * Sharing support link.
+ */
+function sharedaddy_load_more_link() {
+ echo 'https://jetpack.com/support/sharing/';
+}
+add_filter( 'jetpack_learn_more_button_sharedaddy', 'sharedaddy_load_more_link' );
+
+/**
+ * Sharing description.
+ */
+function sharedaddy_more_info() {
+ esc_html_e(
+ 'Visitors can share your posts with Twitter, Facebook, Reddit, Digg, LinkedIn, Google+, print,
+ and email. You can configure services to appear as icons, text, or both and some services like Twitter
+ have additional options.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_sharedaddy', 'sharedaddy_more_info' );
+
+/**
+ * Extra Sidebar Widgets support link.
+ */
+function jetpack_widgets_load_more_link() {
+ echo 'https://jetpack.com/support/extra-sidebar-widgets/';
+}
+add_filter( 'jetpack_learn_more_button_widgets', 'jetpack_widgets_load_more_link' );
+
+/**
+ * Extra Sidebar Widgets description.
+ */
+function jetpack_widgets_more_info() {
+ esc_html_e(
+ 'Add as many custom widgets as you like by dragging and dropping and customize each to fit your needs,
+ including, Twitter streams, Facebook like boxes, custom images, Gravatars, tiled galleries, recent posts,
+ or social icons.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_widgets', 'jetpack_widgets_more_info' );
+
+/**
+ * Subscriptions support link.
+ */
+function jetpack_subscriptions_load_more_link() {
+ echo 'https://jetpack.com/support/subscriptions/';
+}
+add_action( 'jetpack_learn_more_button_subscriptions', 'jetpack_subscriptions_load_more_link' );
+
+/**
+ * Subscriptions description.
+ */
+function jetpack_subscriptions_more_info() {
+ esc_html_e(
+ 'A widget in your sidebar allows visitors to subscribe to your site so that they receive an email
+ each time you publish new content. Your visitors can also subscribe to a post\'s comments to keep up with the conversation.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_subscriptions', 'jetpack_subscriptions_more_info' );
+
+/**
+ * Enhanced Distribution support link.
+ */
+function jetpack_enhanced_distribution_more_link() {
+ echo 'https://jetpack.com/support/enhanced-distribution/';
+}
+add_action( 'jetpack_learn_more_button_enhanced-distribution', 'jetpack_enhanced_distribution_more_link' );
+
+/**
+ * Enhanced Distribution description.
+ */
+function jetpack_enhanced_distribution_more_info() {
+ esc_html_e(
+ 'Jetpack will automatically take your great published content and share it instantly with third-party services
+ like search engines, increasing your reach and traffic.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_enhanced-distribution', 'jetpack_enhanced_distribution_more_info' );
+
+
+/**
+ * Protect support link.
+ */
+function jetpack_protect_more_link() {
+ echo 'https://jetpack.com/support/protect/';
+}
+add_action( 'jetpack_learn_more_button_protect', 'jetpack_protect_more_link' );
+
+/**
+ * Protect description.
+ */
+function jetpack_protect_more_info() {
+ esc_html_e(
+ 'Most sites will come under attack from automated bots that attempt to log in for malicious purposes.
+ We protect you automatically from unauthorized access by using data from millions of sites.',
+ 'jetpack'
+ );
+}
+
+add_action( 'jetpack_module_more_info_protect', 'jetpack_protect_more_info' );
+
+/**
+ * JSON API support link.
+ */
+function jetpack_json_api_more_link() {
+ echo 'https://jetpack.com/support/json-api/';
+}
+add_action( 'jetpack_learn_more_button_json-api', 'jetpack_json_api_more_link' );
+
+/**
+ * JSON API description.
+ */
+function jetpack_json_api_more_info() {
+ esc_html_e(
+ 'Authorize applications and services to securely connect to your site. Developers can use WordPress.com\'s OAuth2
+ authentication system and WordPress.com REST API to manage and access your site\'s content.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_json-api', 'jetpack_json_api_more_info' );
+
+/**
+ * Contact Form support link.
+ */
+function jetpack_contact_form_learn_more_button() {
+ echo 'https://jetpack.com/support/contact-form/';
+}
+add_action( 'jetpack_learn_more_button_contact-form', 'jetpack_contact_form_learn_more_button' );
+
+/**
+ * Contact Form description.
+ */
+function jetpack_contact_form_more_info() {
+ esc_html_e(
+ 'Create simple contact forms without any coding. You can have multiple forms and when
+ a user submits it, their feedback will be emailed directly to you. If Akismet is active, submissions will be
+ automatically filtered for spam.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_contact-form', 'jetpack_contact_form_more_info' );
+
+/**
+ * Comments support link.
+ */
+function jetpack_comments_learn_more_button() {
+ echo 'https://jetpack.com/support/comments';
+}
+add_action( 'jetpack_learn_more_button_comments', 'jetpack_comments_learn_more_button' );
+
+/**
+ * Comments description.
+ */
+function jetpack_comments_more_info() {
+ esc_html_e(
+ 'Allow visitors to use their WordPress.com, Twitter, or Facebook accounts when commenting on
+ your site. Jetpack will match your site\'s color scheme automatically (but you can adjust that).',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_comments', 'jetpack_comments_more_info' );
+
+/**
+ * Carousel support link.
+ */
+function jetpack_carousel_learn_more_button() {
+ echo 'https://jetpack.com/support/carousel';
+}
+add_action( 'jetpack_learn_more_button_carousel', 'jetpack_carousel_learn_more_button' );
+
+/**
+ * Carousel description.
+ */
+function jetpack_carousel_more_info() {
+ esc_html_e(
+ 'With Carousel active, any standard WordPress galleries or single images you have embedded in posts or pages will
+ launch a full-screen photo browsing experience with comments and EXIF metadata.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_carousel', 'jetpack_carousel_more_info' );
+
+/**
+ * Custom CSS support link.
+ */
+function jetpack_custom_css_more_button() {
+ echo 'https://jetpack.com/support/custom-css';
+}
+add_action( 'jetpack_learn_more_button_custom-css', 'jetpack_custom_css_more_button' );
+
+/**
+ * Custom CSS description.
+ */
+function jetpack_custom_css_more_info() {
+ esc_html_e(
+ "Add to or replace your theme's CSS including mobile styles, LESS, and SaSS.
+ Includes syntax coloring, auto-indentation, and immediate CSS validation.",
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_custom-css', 'jetpack_custom_css_more_info' );
+
+/**
+ * Masterbar support link.
+ */
+function jetpack_masterbar_more_link() {
+ echo 'https://jetpack.com/support/masterbar/';
+}
+add_action( 'jetpack_learn_more_button_masterbar', 'jetpack_masterbar_more_link' );
+
+/**
+ * Masterbar description.
+ */
+function jetpack_masterbar_more_info() {
+ esc_html_e(
+ 'Quickly access your Stats, Notifications, Posts and more on WordPress.com.
+ The Toolbar is displayed for any user on the site that is connected to WordPress.com.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_masterbar', 'jetpack_masterbar_more_info' );
+
+/**
+ * Mobile Theme support link.
+ */
+function jetpack_minileven_more_button() {
+ echo 'https://jetpack.com/support/mobile-theme';
+}
+add_action( 'jetpack_learn_more_button_minileven', 'jetpack_minileven_more_button' );
+
+/**
+ * Mobile Theme description.
+ */
+function jetpack_minileven_more_info() {
+ esc_html_e(
+ "Automatically optimize your site for mobile. Jetpack's mobile theme uses the header image,
+ background, and widgets from your current theme. Post format support means your photos and galleries
+ will also look fantastic.",
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_minileven', 'jetpack_minileven_more_info' );
+
+/**
+ * Infinite Scroll support link.
+ */
+function jetpack_infinite_scroll_more_button() {
+ echo 'https://jetpack.com/support/infinite-scroll';
+}
+add_action( 'jetpack_learn_more_button_infinite-scroll', 'jetpack_infinite_scroll_more_button' );
+
+/**
+ * Infinite Scroll description.
+ */
+function jetpack_infinite_scroll_more_info() {
+ esc_html_e(
+ 'Infinite scrolling pulls the next set of posts automatically into view when the reader approaches
+ the bottom of the page. This helps you reader see more of your content.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_infinite-scroll', 'jetpack_infinite_scroll_more_info' );
+
+/**
+ * Post by Email support link.
+ */
+function jetpack_post_by_email_more_link() {
+ echo 'https://jetpack.com/support/post-by-email/';
+}
+add_action( 'jetpack_learn_more_button_post-by-email', 'jetpack_post_by_email_more_link' );
+
+/**
+ * Post by Email description.
+ */
+function jetpack_post_by_email_more_info() {
+ esc_html_e(
+ 'Publish posts on your site by writing and sending an email from any email client instead of using the post editor.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_post-by-email', 'jetpack_post_by_email_more_info' );
+
+/**
+ * Photon support link.
+ */
+function jetpack_photon_more_link() {
+ echo 'https://jetpack.com/support/photon';
+}
+add_action( 'jetpack_learn_more_button_photon', 'jetpack_photon_more_link' );
+
+/**
+ * Photon description.
+ */
+function jetpack_photon_more_info() {
+ esc_html_e(
+ 'Jetpack will optimize your images and serve them from the server location nearest
+ to your visitors. Using our global content delivery network will boost the loading speed of your site.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_photon', 'jetpack_photon_more_info' );
+
+/**
+ * Lazy Images support link.
+ */
+function jetpack_lazy_images_more_link() {
+ echo 'https://jetpack.com/support/lazy-images/';
+}
+add_action( 'jetpack_learn_more_button_lazy-images', 'jetpack_lazy_images_more_link' );
+
+/**
+ * Lazy Images description.
+ */
+function jetpack_lazy_images_more_info() {
+ esc_html_e(
+ 'Improve your site\'s speed by only loading images visible on the screen.
+ New images will load just before they scroll into view. This prevents viewers
+ from having to download all the images on a page all at once, even ones they can\'t see.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_lazy-images', 'jetpack_lazy_images_more_info' );
+
+/**
+ * Tiled Galleries support link.
+ */
+function jetpack_tiled_gallery_more_link() {
+ echo 'https://jetpack.com/support/tiled-galleries/';
+}
+add_action( 'jetpack_learn_more_button_tiled-gallery', 'jetpack_tiled_gallery_more_link' );
+
+/**
+ * Tiled Galleries description.
+ */
+function jetpack_tiled_gallery_more_info() {
+ esc_html_e(
+ 'When adding an image gallery, you will have the option to create elegant magazine-style mosaic layouts for your photos,
+ including mosaic (default), square, and circular layouts.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_tiled-gallery', 'jetpack_tiled_gallery_more_info' );
+
+/**
+ * Likes support link.
+ */
+function jetpack_likes_more_link() {
+ echo 'https://jetpack.com/support/likes/';
+}
+add_action( 'jetpack_learn_more_button_likes', 'jetpack_likes_more_link' );
+
+/**
+ * Likes description.
+ */
+function jetpack_likes_more_info() {
+ esc_html_e(
+ 'Allow your readers to show their appreciation for your posts and other content. Likes show up
+ below each post and your readers will also be able to review their liked posts from WordPress.com.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_likes', 'jetpack_likes_more_info' );
+
+/**
+ * Widget Visibility support link.
+ */
+function jetpack_widget_visibility_more_link() {
+ echo 'https://jetpack.com/support/widget-visibility/';
+}
+add_action( 'jetpack_learn_more_button_widget-visibility', 'jetpack_widget_visibility_more_link' );
+
+/**
+ * Widget Visibility description.
+ */
+function jetpack_widget_visibility_more_info() {
+ esc_html_e(
+ 'Choose from a set of visibility options for sidebar widgets such as showing them only certain categories,
+ only on error pages, or only search results pages. You can also do the reverse and choose to hide them on certain pages.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_widget-visibility', 'jetpack_widget_visibility_more_info' );
+
+/**
+ * VideoPress support link.
+ */
+function jetpack_videopress_more_link() {
+ echo 'https://jetpack.com/support/videopress/';
+}
+add_action( 'jetpack_learn_more_button_videopress', 'jetpack_videopress_more_link' );
+
+/**
+ * VideoPress description.
+ */
+function jetpack_videopress_more_info() {
+ esc_html_e(
+ 'The easiest way to upload ad-free and unbranded videos to your site. You get stats on video
+ playback and shares and the player is lightweight and responsive.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_videopress', 'jetpack_videopress_more_info' );
+
+/**
+ * SSO support link.
+ */
+function jetpack_sso_more_link() {
+ echo 'https://jetpack.com/support/sso/';
+}
+add_action( 'jetpack_learn_more_button_sso', 'jetpack_sso_more_link' );
+
+/**
+ * SSO description.
+ */
+function jetpack_sso_more_info() {
+ esc_html_e(
+ 'Your users will be able to log in to your site with their WordPress.com account.
+ This includes two-factor authentication making it the safest login mechanism for your site.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_sso', 'jetpack_sso_more_info' );
+
+/**
+ * Monitor support link.
+ */
+function jetpack_monitor_more_link() {
+ echo 'https://jetpack.com/support/monitor/';
+}
+add_action( 'jetpack_learn_more_button_monitor', 'jetpack_monitor_more_link' );
+
+/**
+ * Monitor description.
+ */
+function jetpack_monitor_more_info() {
+ esc_html_e(
+ 'Jetpack checks your site every five minutes and if any downtime is detected you will receive an email
+ notification alerting you to the issue, so you can act quickly and get your site back online.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_monitor', 'jetpack_monitor_more_info' );
+
+/**
+ * Related Posts support link.
+ */
+function jetpack_related_posts_more_button() {
+ echo 'https://jetpack.com/support/related-posts/';
+}
+add_action( 'jetpack_learn_more_button_related-posts', 'jetpack_related_posts_more_button' );
+
+/**
+ * Related Posts description.
+ */
+function jetpack_related_posts_more_info() {
+ esc_html_e(
+ 'Show visitors related content from your site at the bottom of your posts. This encourages them
+ to browse more content, explore your site, and transform them into regular readers.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_related-posts', 'jetpack_related_posts_more_info' );
+
+/**
+ * Markdown support link.
+ */
+function jetpack_markdown_more_link() {
+ echo 'https://jetpack.com/support/markdown/';
+}
+add_action( 'jetpack_learn_more_button_markdown', 'jetpack_markdown_more_link' );
+
+/**
+ * Markdown description.
+ */
+function jetpack_markdown_more_info() {
+ esc_html_e(
+ 'Compose posts and comments with links, lists, and other styles using regular characters and
+ punctuation marks. A quick and easy way to format text without needing any HTML or coding.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_markdown', 'jetpack_markdown_more_info' );
+
+/**
+ * Site Verification Tools support link.
+ */
+function jetpack_verification_tools_more_link() {
+ echo 'https://jetpack.com/support/site-verification-tools/';
+}
+add_action( 'jetpack_learn_more_button_verification-tools', 'jetpack_verification_tools_more_link' );
+
+/**
+ * Site Verification Tools description.
+ */
+function jetpack_verification_tools_more_info() {
+ esc_html_e(
+ 'Verify your site ownership with services like Google, Bing, Pinterest, and Yandex. This gives you access to
+ advanced features on these services and get verification badges.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_verification-tools', 'jetpack_verification_tools_more_info' );
+
+/**
+ * SEO Tools support link.
+ */
+function jetpack_seo_tools_more_link() {
+ echo 'https://jetpack.com/support/seo-tools/';
+}
+add_action( 'jetpack_learn_more_button_seo-tools', 'jetpack_seo_tools_more_link' );
+
+/**
+ * SEO Tools description.
+ */
+function jetpack_seo_tools_more_info() {
+ esc_html_e(
+ 'Better results on search engines and social media.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_seo-tools', 'jetpack_seo_tools_more_info' );
+
+/**
+ * Custom Content Types support link.
+ */
+function jetpack_custom_content_types_more_link() {
+ echo 'https://jetpack.com/support/custom-content-types/';
+}
+add_action( 'jetpack_learn_more_button_custom-content-types', 'jetpack_custom_content_types_more_link' );
+
+/**
+ * Custom Content Types description.
+ */
+function jetpack_custom_content_types_more_info() {
+ esc_html_e(
+ 'Add and organize content that doesn’t necessarily fit into a post or static page such as portfolios
+ or testimonials. Custom content can be visible at specific URLs, or you may add them with shortcodes.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_custom-content-types', 'jetpack_custom_content_types_more_info' );
+
+/**
+ * Manage support link.
+ */
+function jetpack_manage_more_link() {
+ echo 'https://jetpack.com/support/site-management/';
+}
+add_action( 'jetpack_learn_more_button_manage', 'jetpack_manage_more_link' );
+
+/**
+ * Manage description.
+ */
+function jetpack_custom_jetpack_manage() {
+ esc_html_e(
+ 'Manage and update this and other WordPress sites from one simple dashboard on WordPress.com. You can update
+ plugins, set them to automatically update, and (de)activate them on a per-site basis or in bulk from
+ wordpress.com/plugins. You can also use the brand new and mobile-friendly post editor on WordPress.com as well
+ as view and activate installed themes and create or edit site menus.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_manage', 'jetpack_custom_jetpack_manage' );
+
+/**
+ * Sitemaps support link.
+ */
+function jetpack_sitemaps_more_link() {
+ echo 'https://jetpack.com/support/sitemaps/';
+}
+add_action( 'jetpack_learn_more_button_sitemaps', 'jetpack_sitemaps_more_link' );
+
+/**
+ * Sitemaps description.
+ */
+function jetpack_xml_sitemap_more_info() {
+ esc_html_e(
+ 'Automatically create two sitemap files that list the URLs of posts and pages in your site.
+ This makes it easier for search engines (like Google) to include your site in relevant search results.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_sitemaps', 'jetpack_xml_sitemap_more_info' );
+
+/**
+ * WordAds support link.
+ */
+function jetpack_wordads_more_link() {
+ echo 'https://wordads.co/';
+}
+add_action( 'jetpack_learn_more_button_wordads', 'jetpack_wordads_more_link' );
+
+/**
+ * WordAds description.
+ */
+function jetpack_wordads_more_info() {
+ esc_html_e(
+ 'By default ads are shown at the end of every page, post, or the first article on your front page. You can also add them to the top of your site and to any widget area to increase your earnings!',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_wordads', 'jetpack_wordads_more_info' );
+
+/**
+ * Google Analytics support link.
+ */
+function jetpack_google_analytics_more_link() {
+ echo 'https://jetpack.com/support/google-analytics';
+}
+add_action( 'jetpack_learn_more_button_google-analytics', 'jetpack_google_analytics_more_link' );
+
+/**
+ * Google Analytics description.
+ */
+function jetpack_google_analytics_more_info() {
+ esc_html_e(
+ 'Track website statistics with Google Analytics for a deeper understanding of your website visitors and customers.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_google-analytics', 'jetpack_google_analytics_more_info' );
+
+/**
+ * WooCommerce Analytics support link.
+ */
+function jetpack_woocommerce_analytics_more_link() {
+ echo 'https://jetpack.com/support/';
+}
+add_action( 'jetpack_learn_more_button_woocommerce-analytics', 'jetpack_woocommerce_analytics_more_link' );
+
+/**
+ * WooCommerce Analytics description.
+ */
+function jetpack_woocommerce_analytics_more_info() {
+ esc_html_e(
+ 'Enhanced analytics for WooCommerce and Jetpack users.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_woocommerce-analytics', 'jetpack_woocommerce_analytics_more_info' );
+
+/**
+ * Search support link.
+ */
+function jetpack_search_more_link() {
+ echo 'https://jetpack.com/support/search/';
+}
+add_action( 'jetpack_learn_more_button_search', 'jetpack_search_more_link' );
+
+/**
+ * Search description.
+ */
+function jetpack_search_more_info() {
+ esc_html_e(
+ 'Enhanced search, powered by Elasticsearch, a powerful replacement for WordPress search.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_search', 'jetpack_search_more_info' );
+
+/**
+ * Comment Likes support link.
+ */
+function jetpack_comment_likes_more_link() {
+ echo 'https://jetpack.com/support/comment-likes/';
+}
+add_action( 'jetpack_learn_more_button_comment-likes', 'jetpack_comment_likes_more_link' );
+
+/**
+ * Comment Likes description.
+ */
+function jetpack_comment_likes_more_info() {
+ esc_html_e(
+ 'Increase visitor engagement by adding a Like button to comments.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_comment-likes', 'jetpack_comment_likes_more_info' );
+
+/**
+ * Progressive Web Apps support link.
+ */
+function jetpack_pwa_more_link() {
+ echo 'https://jetpack.com/support/progressive-web-apps/';
+}
+add_action( 'jetpack_learn_more_button_pwa', 'jetpack_pwa_more_link' );
+
+/**
+ * Progressive Web Apps description.
+ */
+function jetpack_pwa_more_info() {
+ esc_html_e(
+ 'Speed up and improve the reliability of your site using the latest in web technology.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_pwa', 'jetpack_pwa_more_info' );
+
+/**
+ * Asset CDN support link.
+ */
+function jetpack_assetcdn_more_link() {
+ echo 'https://jetpack.com/support/asset-cdn/';
+}
+add_action( 'jetpack_learn_more_button_photon-cdn', 'jetpack_assetcdn_more_link' );
+
+/**
+ * Asset CDN description.
+ */
+function jetpack_assetcdn_more_info() {
+ esc_html_e(
+ 'Our asset CDN is a site acceleration service.
+ That means that we host static assets like JavaScript and CSS shipped with WordPress Core and Jetpack from our servers, alleviating the load on your server.',
+ 'jetpack'
+ );
+}
+add_action( 'jetpack_module_more_info_photon-cdn', 'jetpack_assetcdn_more_info' );
+
+/**
+ * Copy Post support link.
+ */
+function jetpack_copy_post_more_link() {
+ echo 'https://jetpack.com/support/copy-post-2/';
+}
+add_action( 'jetpack_learn_more_button_copy-post', 'jetpack_copy_post_more_link' );
+
+/**
+ * Copy Post description.
+ */
+function jetpack_more_info_copy_post() {
+ esc_html_e( 'Create a new post based on an existing post.', 'jetpack' );
+}
+add_action( 'jetpack_module_more_info_copy-post', 'jetpack_more_info_copy_post' );
diff --git a/plugins/jetpack/modules/monitor.php b/plugins/jetpack/modules/monitor.php
new file mode 100644
index 00000000..4e9f01e8
--- /dev/null
+++ b/plugins/jetpack/modules/monitor.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * Module Name: Monitor
+ * Module Description: Jetpack’s downtime monitoring will continuously watch your site, and alert you the moment that downtime is detected.
+ * Jumpstart Description: Receive immediate notifications if your site goes down, 24/7.
+ * Sort Order: 28
+ * Recommendation Order: 10
+ * First Introduced: 2.6
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Recommended
+ * Feature: Security, Jumpstart
+ * Additional Search Queries: monitor, uptime, downtime, monitoring, maintenance, maintenance mode, offline, site is down, site down, down, repair, error
+ */
+
+class Jetpack_Monitor {
+
+ public $module = 'monitor';
+
+ function __construct() {
+ add_action( 'jetpack_modules_loaded', array( $this, 'jetpack_modules_loaded' ) );
+ add_action( 'jetpack_activate_module_monitor', array( $this, 'activate_module' ) );
+ }
+
+ public function activate_module() {
+ if ( Jetpack::is_user_connected() ) {
+ self::update_option_receive_jetpack_monitor_notification( true );
+ }
+ }
+
+ public function jetpack_modules_loaded() {
+ Jetpack::enable_module_configurable( $this->module );
+ }
+
+ public function is_active() {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id()
+ ) );
+ $xml->query( 'jetpack.monitor.isActive' );
+ if ( $xml->isError() ) {
+ wp_die( sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
+ }
+ return $xml->getResponse();
+ }
+
+ public function update_option_receive_jetpack_monitor_notification( $value ) {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id()
+ ) );
+ $xml->query( 'jetpack.monitor.setNotifications', (bool) $value );
+
+ if ( $xml->isError() ) {
+ wp_die( sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
+ }
+
+ // To be used only in Jetpack_Core_Json_Api_Endpoints::get_remote_value.
+ update_option( 'monitor_receive_notifications', (bool) $value );
+
+ return true;
+ }
+
+ /**
+ * Checks the status of notifications for current Jetpack site user.
+ *
+ * @since 2.8
+ * @since 4.1.0 New parameter $die_on_error.
+ *
+ * @param bool $die_on_error Whether to issue a wp_die when an error occurs or return a WP_Error object.
+ *
+ * @return boolean|WP_Error
+ */
+ static function user_receives_notifications( $die_on_error = true ) {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id()
+ ) );
+ $xml->query( 'jetpack.monitor.isUserInNotifications' );
+
+ if ( $xml->isError() ) {
+ if ( $die_on_error ) {
+ wp_die( sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
+ } else {
+ return new WP_Error( $xml->getErrorCode(), $xml->getErrorMessage(), array( 'status' => 400 ) );
+ }
+ }
+ return $xml->getResponse();
+ }
+
+ public function activate_monitor() {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id()
+ ) );
+
+ $xml->query( 'jetpack.monitor.activate' );
+
+ if ( $xml->isError() ) {
+ wp_die( sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
+ }
+ return true;
+ }
+
+ public function deactivate_monitor() {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id()
+ ) );
+
+ $xml->query( 'jetpack.monitor.deactivate' );
+
+ if ( $xml->isError() ) {
+ wp_die( sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
+ }
+ return true;
+ }
+
+ /*
+ * Returns date of the last downtime.
+ *
+ * @since 4.0.0
+ * @return date in YYYY-MM-DD HH:mm:ss format
+ */
+ public function monitor_get_last_downtime() {
+// if ( $last_down = get_transient( 'monitor_last_downtime' ) ) {
+// return $last_down;
+// }
+
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id()
+ ) );
+
+ $xml->query( 'jetpack.monitor.getLastDowntime' );
+
+ if ( $xml->isError() ) {
+ return new WP_Error( 'monitor-downtime', $xml->getErrorMessage() );
+ }
+
+ set_transient( 'monitor_last_downtime', $xml->getResponse(), 10 * MINUTE_IN_SECONDS );
+
+ return $xml->getResponse();
+ }
+
+}
+
+new Jetpack_Monitor;
diff --git a/plugins/jetpack/modules/notes.php b/plugins/jetpack/modules/notes.php
new file mode 100644
index 00000000..3b7ddfaa
--- /dev/null
+++ b/plugins/jetpack/modules/notes.php
@@ -0,0 +1,198 @@
+<?php
+/**
+ * Module Name: Notifications
+ * Module Description: Receive instant notifications of site comments and likes.
+ * Sort Order: 13
+ * First Introduced: 1.9
+ * Requires Connection: Yes
+ * Auto Activate: Yes
+ * Module Tags: Other
+ * Feature: General
+ * Additional Search Queries: notification, notifications, toolbar, adminbar, push, comments
+ */
+
+if ( !defined( 'JETPACK_NOTES__CACHE_BUSTER' ) ) define( 'JETPACK_NOTES__CACHE_BUSTER', JETPACK__VERSION . '-' . gmdate( 'oW' ) );
+
+class Jetpack_Notifications {
+ public $jetpack = false;
+
+ /**
+ * Singleton
+ * @static
+ */
+ public static function init() {
+ static $instance = array();
+
+ if ( !$instance ) {
+ $instance[0] = new Jetpack_Notifications;
+ }
+
+ return $instance[0];
+ }
+
+ function __construct() {
+ $this->jetpack = Jetpack::init();
+
+ add_action( 'init', array( &$this, 'action_init' ) );
+ }
+
+ function wpcom_static_url($file) {
+ $i = hexdec( substr( md5( $file ), -1 ) ) % 2;
+ $url = 'http://s' . $i . '.wp.com' . $file;
+ return set_url_scheme( $url );
+ }
+
+ // return the major version of Internet Explorer the viewer is using or false if it's not IE
+ public static function get_internet_explorer_version() {
+ static $version;
+ if ( isset( $version ) ) {
+ return $version;
+ }
+
+ $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
+
+ preg_match( '/MSIE (\d+)/', $user_agent, $matches );
+ $version = empty( $matches[1] ) ? null : $matches[1];
+ if ( empty( $version ) || !$version ) {
+ return false;
+ }
+ return $version;
+ }
+
+ public static function current_browser_is_supported() {
+ static $supported;
+
+ if ( isset( $supported ) ) {
+ return $supported;
+ }
+
+ $ie_version = self::get_internet_explorer_version();
+ if ( false === $ie_version ) {
+ return $supported = true;
+ }
+
+ if ( $ie_version < 8 ) {
+ return $supported = false;
+ }
+
+ return $supported = true;
+ }
+
+ function action_init() {
+ //syncing must wait until after init so
+ //post types that support comments
+ $filt_post_types = array();
+ $all_post_types = get_post_types();
+ foreach ( $all_post_types as $post_type ) {
+ if ( post_type_supports( $post_type, 'comments' ) ) {
+ $filt_post_types[] = $post_type;
+ }
+ }
+
+ if ( defined( 'DOING_AJAX' ) && DOING_AJAX )
+ return;
+
+ if ( !has_filter( 'show_admin_bar', '__return_true' ) && !is_user_logged_in() )
+ return;
+
+ if ( !self::current_browser_is_supported() )
+ return;
+
+ add_action( 'admin_bar_menu', array( &$this, 'admin_bar_menu'), 120 );
+ add_action( 'wp_head', array( &$this, 'styles_and_scripts'), 120 );
+ add_action( 'admin_head', array( &$this, 'styles_and_scripts') );
+ }
+
+ function styles_and_scripts() {
+ $is_rtl = is_rtl();
+
+ if ( Jetpack::is_module_active( 'masterbar' ) ) {
+ /**
+ * Can be used to force Notifications to display in RTL style.
+ *
+ * @module notes
+ *
+ * @since 4.8.0
+ *
+ * @param bool true Should notifications be displayed in RTL style. Defaults to false.
+ */
+ $is_rtl = apply_filters( 'a8c_wpcom_masterbar_enqueue_rtl_notification_styles', false );
+ }
+
+ if ( ! $is_rtl ) {
+ wp_enqueue_style( 'wpcom-notes-admin-bar', $this->wpcom_static_url( '/wp-content/mu-plugins/notes/admin-bar-v2.css' ), array(), JETPACK_NOTES__CACHE_BUSTER );
+ } else {
+ wp_enqueue_style( 'wpcom-notes-admin-bar', $this->wpcom_static_url( '/wp-content/mu-plugins/notes/rtl/admin-bar-v2-rtl.css' ), array(), JETPACK_NOTES__CACHE_BUSTER );
+ }
+ wp_enqueue_style( 'noticons', $this->wpcom_static_url( '/i/noticons/noticons.css' ), array(), JETPACK_NOTES__CACHE_BUSTER );
+
+ $this->print_js();
+
+ // attempt to use core or plugin libraries if registered
+ if ( !wp_script_is( 'mustache', 'registered' ) ) {
+ wp_register_script( 'mustache', $this->wpcom_static_url( '/wp-content/js/mustache.js' ), null, JETPACK_NOTES__CACHE_BUSTER );
+ }
+ if ( !wp_script_is( 'underscore', 'registered' ) ) {
+ wp_register_script( 'underscore', $this->wpcom_static_url( '/wp-includes/js/underscore.min.js' ), null, JETPACK_NOTES__CACHE_BUSTER );
+ }
+ if ( !wp_script_is( 'backbone', 'registered' ) ) {
+ wp_register_script( 'backbone', $this->wpcom_static_url( '/wp-includes/js/backbone.min.js' ), array( 'underscore' ), JETPACK_NOTES__CACHE_BUSTER );
+ }
+
+ wp_register_script( 'wpcom-notes-common', $this->wpcom_static_url( '/wp-content/mu-plugins/notes/notes-common-v2.js' ), array( 'jquery', 'underscore', 'backbone', 'mustache' ), JETPACK_NOTES__CACHE_BUSTER );
+ wp_enqueue_script( 'wpcom-notes-admin-bar', $this->wpcom_static_url( '/wp-content/mu-plugins/notes/admin-bar-v2.js' ), array( 'wpcom-notes-common' ), JETPACK_NOTES__CACHE_BUSTER );
+ }
+
+ function admin_bar_menu() {
+ global $wp_admin_bar, $current_blog;
+
+ if ( !is_object( $wp_admin_bar ) )
+ return;
+
+ $wpcom_locale = get_locale();
+
+ if ( !class_exists( 'GP_Locales' ) ) {
+ if ( defined( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) && file_exists( JETPACK__GLOTPRESS_LOCALES_PATH ) ) {
+ require JETPACK__GLOTPRESS_LOCALES_PATH;
+ }
+ }
+
+ if ( class_exists( 'GP_Locales' ) ) {
+ $wpcom_locale_object = GP_Locales::by_field( 'wp_locale', $wpcom_locale );
+ if ( $wpcom_locale_object instanceof GP_Locale ) {
+ $wpcom_locale = $wpcom_locale_object->slug;
+ }
+ }
+
+ $classes = 'wpnt-loading wpn-read';
+ $wp_admin_bar->add_menu( array(
+ 'id' => 'notes',
+ 'title' => '<span id="wpnt-notes-unread-count" class="' . esc_attr( $classes ) . '">
+ <span class="noticon noticon-notification"></span>
+ </span>',
+ 'meta' => array(
+ 'html' => '<div id="wpnt-notes-panel2" style="display:none" lang="'. esc_attr( $wpcom_locale ) . '" dir="' . ( is_rtl() ? 'rtl' : 'ltr' ) . '"><div class="wpnt-notes-panel-header"><span class="wpnt-notes-header">' . __( 'Notifications', 'jetpack' ) . '</span><span class="wpnt-notes-panel-link"></span></div></div>',
+ 'class' => 'menupop',
+ ),
+ 'parent' => 'top-secondary',
+ ) );
+ }
+
+ function print_js() {
+ $link_accounts_url = is_user_logged_in() && !Jetpack::is_user_connected() ? Jetpack::admin_url() : false;
+?>
+<script type="text/javascript">
+/* <![CDATA[ */
+ var wpNotesIsJetpackClient = true;
+ var wpNotesIsJetpackClientV2 = true;
+<?php if ( $link_accounts_url ) : ?>
+ var wpNotesLinkAccountsURL = '<?php print $link_accounts_url; ?>';
+<?php endif; ?>
+/* ]]> */
+</script>
+<?php
+ }
+
+}
+
+Jetpack_Notifications::init();
diff --git a/plugins/jetpack/modules/omnisearch.php b/plugins/jetpack/modules/omnisearch.php
new file mode 100644
index 00000000..88c13090
--- /dev/null
+++ b/plugins/jetpack/modules/omnisearch.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer needed.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/photon-cdn.php b/plugins/jetpack/modules/photon-cdn.php
new file mode 100644
index 00000000..ecf0288c
--- /dev/null
+++ b/plugins/jetpack/modules/photon-cdn.php
@@ -0,0 +1,298 @@
+<?php
+/**
+ * Module Name: Asset CDN
+ * Module Description: Jetpack’s Site Accelerator loads your site faster by optimizing your images and serving your images and static files from our global network of servers.
+ * Sort Order: 26
+ * Recommendation Order: 1
+ * First Introduced: 6.6
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Photos and Videos, Appearance, Recommended
+ * Feature: Recommended, Appearance, Jumpstart
+ * Additional Search Queries: site accelerator, accelerate, static, assets, javascript, css, files, performance, cdn, bandwidth, content delivery network, pagespeed, combine js, optimize css
+ */
+
+$GLOBALS['concatenate_scripts'] = false;
+
+Jetpack::dns_prefetch( array(
+ '//c0.wp.com',
+) );
+
+class Jetpack_Photon_Static_Assets_CDN {
+ const CDN = 'https://c0.wp.com/';
+
+ /**
+ * Sets up action handlers needed for Jetpack CDN.
+ */
+ public static function go() {
+ add_action( 'wp_print_scripts', array( __CLASS__, 'cdnize_assets' ) );
+ add_action( 'wp_print_styles', array( __CLASS__, 'cdnize_assets' ) );
+ add_action( 'admin_print_scripts', array( __CLASS__, 'cdnize_assets' ) );
+ add_action( 'admin_print_styles', array( __CLASS__, 'cdnize_assets' ) );
+ add_action( 'wp_footer', array( __CLASS__, 'cdnize_assets' ) );
+ add_filter( 'load_script_textdomain_relative_path', array( __CLASS__, 'fix_script_relative_path' ), 10, 2 );
+ }
+
+ /**
+ * Sets up CDN URLs for assets that are enqueued by the WordPress Core.
+ */
+ public static function cdnize_assets() {
+ global $wp_scripts, $wp_styles, $wp_version;
+
+ /*
+ * Short-circuit if AMP since not relevant as custom JS is not allowed and CSS is inlined.
+ * Note that it is not suitable to use the jetpack_force_disable_site_accelerator filter for this
+ * because it will be applied before the wp action, which is the point at which the queried object
+ * is available and we know whether the response will be AMP or not. This is particularly important
+ * for AMP-first (native AMP) pages where there are no AMP-specific URLs.
+ */
+ if ( Jetpack_AMP_Support::is_amp_request() ) {
+ return;
+ }
+
+ /**
+ * Filters Jetpack CDN's Core version number and locale. Can be used to override the values
+ * that Jetpack uses to retrieve assets. Expects the values to be returned in an array.
+ *
+ * @module photon-cdn
+ *
+ * @since 6.6.0
+ *
+ * @param array $values array( $version = core assets version, i.e. 4.9.8, $locale = desired locale )
+ */
+ list( $version, $locale ) = apply_filters(
+ 'jetpack_cdn_core_version_and_locale',
+ array( $wp_version, get_locale() )
+ );
+
+ if ( self::is_public_version( $version ) ) {
+ $site_url = trailingslashit( site_url() );
+ foreach ( $wp_scripts->registered as $handle => $thing ) {
+ if ( wp_startswith( $thing->src, self::CDN ) ) {
+ continue;
+ }
+ $src = ltrim( str_replace( $site_url, '', $thing->src ), '/' );
+ if ( self::is_js_or_css_file( $src ) && in_array( substr( $src, 0, 9 ), array( 'wp-admin/', 'wp-includ' ) ) ) {
+ $wp_scripts->registered[ $handle ]->src = sprintf( self::CDN . 'c/%1$s/%2$s', $version, $src );
+ $wp_scripts->registered[ $handle ]->ver = null;
+ }
+ }
+ foreach ( $wp_styles->registered as $handle => $thing ) {
+ if ( wp_startswith( $thing->src, self::CDN ) ) {
+ continue;
+ }
+ $src = ltrim( str_replace( $site_url, '', $thing->src ), '/' );
+ if ( self::is_js_or_css_file( $src ) && in_array( substr( $src, 0, 9 ), array( 'wp-admin/', 'wp-includ' ) ) ) {
+ $wp_styles->registered[ $handle ]->src = sprintf( self::CDN . 'c/%1$s/%2$s', $version, $src );
+ $wp_styles->registered[ $handle ]->ver = null;
+ }
+ }
+ }
+
+ self::cdnize_plugin_assets( 'jetpack', JETPACK__VERSION );
+ if ( class_exists( 'WooCommerce' ) ) {
+ self::cdnize_plugin_assets( 'woocommerce', WC_VERSION );
+ }
+ }
+
+ /**
+ * Ensure use of the correct relative path when determining the JavaScript file names.
+ *
+ * @param string $relative The relative path of the script. False if it could not be determined.
+ * @param string $src The full source url of the script.
+ * @return string The expected relative path for the CDN-ed URL.
+ */
+ public static function fix_script_relative_path( $relative, $src ) {
+
+ // Note relevant in AMP responses. See note above.
+ if ( Jetpack_AMP_Support::is_amp_request() ) {
+ return $relative;
+ }
+
+ $strpos = strpos( $src, '/wp-includes/' );
+
+ // We only treat URLs that have wp-includes in them. Cases like language textdomains
+ // can also use this filter, they don't need to be touched because they are local paths.
+ if ( false === $strpos ) {
+ return $relative;
+ }
+ return substr( $src, 1 + $strpos );
+ }
+
+ /**
+ * Sets up CDN URLs for supported plugin assets.
+ *
+ * @param String $plugin_slug plugin slug string.
+ * @param String $current_version plugin version string.
+ * @return null|bool
+ */
+ public static function cdnize_plugin_assets( $plugin_slug, $current_version ) {
+ global $wp_scripts, $wp_styles;
+
+ /**
+ * Filters Jetpack CDN's plugin slug and version number. Can be used to override the values
+ * that Jetpack uses to retrieve assets. For example, when testing a development version of Jetpack
+ * the assets are not yet published, so you may need to override the version value to either
+ * trunk, or the latest available version. Expects the values to be returned in an array.
+ *
+ * @module photon-cdn
+ *
+ * @since 6.6.0
+ *
+ * @param array $values array( $slug = the plugin repository slug, i.e. jetpack, $version = the plugin version, i.e. 6.6 )
+ */
+ list( $plugin_slug, $current_version ) = apply_filters(
+ 'jetpack_cdn_plugin_slug_and_version',
+ array( $plugin_slug, $current_version )
+ );
+
+ $assets = self::get_plugin_assets( $plugin_slug, $current_version );
+ $plugin_directory_url = plugins_url() . '/' . $plugin_slug . '/';
+
+ if ( is_wp_error( $assets ) || ! is_array( $assets ) ) {
+ return false;
+ }
+
+ foreach ( $wp_scripts->registered as $handle => $thing ) {
+ if ( wp_startswith( $thing->src, self::CDN ) ) {
+ continue;
+ }
+ if ( wp_startswith( $thing->src, $plugin_directory_url ) ) {
+ $local_path = substr( $thing->src, strlen( $plugin_directory_url ) );
+ if ( in_array( $local_path, $assets, true ) ) {
+ $wp_scripts->registered[ $handle ]->src = sprintf( self::CDN . 'p/%1$s/%2$s/%3$s', $plugin_slug, $current_version, $local_path );
+ $wp_scripts->registered[ $handle ]->ver = null;
+ }
+ }
+ }
+ foreach ( $wp_styles->registered as $handle => $thing ) {
+ if ( wp_startswith( $thing->src, self::CDN ) ) {
+ continue;
+ }
+ if ( wp_startswith( $thing->src, $plugin_directory_url ) ) {
+ $local_path = substr( $thing->src, strlen( $plugin_directory_url ) );
+ if ( in_array( $local_path, $assets, true ) ) {
+ $wp_styles->registered[ $handle ]->src = sprintf( self::CDN . 'p/%1$s/%2$s/%3$s', $plugin_slug, $current_version, $local_path );
+ $wp_styles->registered[ $handle ]->ver = null;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns cdn-able assets for a given plugin.
+ *
+ * @param string $plugin plugin slug string.
+ * @param string $version plugin version number string.
+ * @return array|bool Will return false if not a public version.
+ */
+ public static function get_plugin_assets( $plugin, $version ) {
+ if ( 'jetpack' === $plugin && JETPACK__VERSION === $version ) {
+ if ( ! self::is_public_version( $version ) ) {
+ return false;
+ }
+
+ $assets = array(); // The variable will be redefined in the included file.
+
+ include JETPACK__PLUGIN_DIR . 'modules/photon-cdn/jetpack-manifest.php';
+ return $assets;
+ }
+
+ /**
+ * Used for other plugins to provide their bundled assets via filter to
+ * prevent the need of storing them in an option or an external api request
+ * to w.org.
+ *
+ * @module photon-cdn
+ *
+ * @since 6.6.0
+ *
+ * @param array $assets The assets array for the plugin.
+ * @param string $version The version of the plugin being requested.
+ */
+ $assets = apply_filters( "jetpack_cdn_plugin_assets-{$plugin}", null, $version );
+ if ( is_array( $assets ) ) {
+ return $assets;
+ }
+
+ if ( ! self::is_public_version( $version ) ) {
+ return false;
+ }
+
+ $cache = Jetpack_Options::get_option( 'static_asset_cdn_files', array() );
+ if ( isset( $cache[ $plugin ][ $version ] ) ) {
+ if ( is_array( $cache[ $plugin ][ $version ] ) ) {
+ return $cache[ $plugin ][ $version ];
+ }
+ if ( is_numeric( $cache[ $plugin ][ $version ] ) ) {
+ // Cache an empty result for up to 24h.
+ if ( intval( $cache[ $plugin ][ $version ] ) + DAY_IN_SECONDS > time() ) {
+ return array();
+ }
+ }
+ }
+
+ $url = sprintf( 'http://downloads.wordpress.org/plugin-checksums/%s/%s.json', $plugin, $version );
+
+ if ( wp_http_supports( array( 'ssl' ) ) ) {
+ $url = set_url_scheme( $url, 'https' );
+ }
+
+ $response = wp_remote_get( $url );
+
+ $body = trim( wp_remote_retrieve_body( $response ) );
+ $body = json_decode( $body, true );
+
+ $return = time();
+ if ( is_array( $body ) ) {
+ $return = array_filter( array_keys( $body['files'] ), array( __CLASS__, 'is_js_or_css_file' ) );
+ }
+
+ $cache[ $plugin ] = array();
+ $cache[ $plugin ][ $version ] = $return;
+ Jetpack_Options::update_option( 'static_asset_cdn_files', $cache, true );
+
+ return $return;
+ }
+
+ /**
+ * Checks a path whether it is a JS or CSS file.
+ *
+ * @param String $path file path.
+ * @return Boolean whether the file is a JS or CSS.
+ */
+ public static function is_js_or_css_file( $path ) {
+ return ( false === strpos( $path, '?' ) ) && in_array( substr( $path, -3 ), array( 'css', '.js' ), true );
+ }
+
+ /**
+ * Checks whether the version string indicates a production version.
+ *
+ * @param String $version the version string.
+ * @param Boolean $include_beta_and_rc whether to count beta and RC versions as production.
+ * @return Boolean
+ */
+ public static function is_public_version( $version, $include_beta_and_rc = false ) {
+ if ( preg_match( '/^\d+(\.\d+)+$/', $version ) ) {
+ // matches `1` `1.2` `1.2.3`.
+ return true;
+ } elseif ( $include_beta_and_rc && preg_match( '/^\d+(\.\d+)+(-(beta|rc|pressable)\d?)$/i', $version ) ) {
+ // matches `1.2.3` `1.2.3-beta` `1.2.3-pressable` `1.2.3-beta1` `1.2.3-rc` `1.2.3-rc2`.
+ return true;
+ }
+ // unrecognized version.
+ return false;
+ }
+}
+/**
+ * Allow plugins to short-circuit the Asset CDN, even when the module is on.
+ *
+ * @module photon-cdn
+ *
+ * @since 6.7.0
+ *
+ * @param false bool Should the Asset CDN be blocked? False by default.
+ */
+if ( true !== apply_filters( 'jetpack_force_disable_site_accelerator', false ) ) {
+ Jetpack_Photon_Static_Assets_CDN::go();
+}
diff --git a/plugins/jetpack/modules/photon-cdn/jetpack-manifest.php b/plugins/jetpack/modules/photon-cdn/jetpack-manifest.php
new file mode 100644
index 00000000..b73bd2c6
--- /dev/null
+++ b/plugins/jetpack/modules/photon-cdn/jetpack-manifest.php
@@ -0,0 +1,418 @@
+<?php
+// This file is autogenerated by bin/build-asset-cdn-json.php
+
+$assets = array (
+ 0 => 'css/jetpack-admin.min.css',
+ 1 => 'css/jetpack-idc.min.css',
+ 2 => 'css/dashboard-widget-rtl.min.css',
+ 3 => 'css/jetpack-admin-rtl.css',
+ 4 => 'css/jetpack-idc-rtl.css',
+ 5 => 'css/jetpack-icons-rtl.css',
+ 6 => 'css/jetpack-icons.min.css',
+ 7 => 'css/jetpack-idc-admin-bar.min.css',
+ 8 => 'css/jetpack-admin.css',
+ 9 => 'css/jetpack-idc-admin-bar-rtl.css',
+ 10 => 'css/jetpack-idc-admin-bar-rtl.min.css',
+ 11 => 'css/jetpack-idc-rtl.min.css',
+ 12 => 'css/jetpack-icons.css',
+ 13 => 'css/jetpack-admin-jitm-rtl.min.css',
+ 14 => 'css/jetpack.css',
+ 15 => 'css/jetpack-admin-jitm.css',
+ 16 => 'css/jetpack-icons-rtl.min.css',
+ 17 => 'css/jetpack-idc.css',
+ 18 => 'css/jetpack-banners-rtl.min.css',
+ 19 => 'css/dashboard-widget.min.css',
+ 20 => 'css/jetpack-admin-rtl.min.css',
+ 21 => 'css/dashboard-widget-rtl.css',
+ 22 => 'css/jetpack-banners.css',
+ 23 => 'css/jetpack-banners-rtl.css',
+ 24 => 'css/jetpack-banners.min.css',
+ 25 => 'css/jetpack-idc-admin-bar.css',
+ 26 => 'css/jetpack-rtl.css',
+ 27 => 'css/jetpack-admin-jitm-rtl.css',
+ 28 => 'css/dashboard-widget.css',
+ 29 => 'css/jetpack-admin-jitm.min.css',
+ 30 => '3rd-party/debug-bar/debug-bar.css',
+ 31 => '3rd-party/debug-bar/debug-bar.js',
+ 32 => '_inc/blocks/vendors~swiper.59b62a96313990494c44.js',
+ 33 => '_inc/blocks/tiled-gallery/view.rtl.css',
+ 34 => '_inc/blocks/tiled-gallery/view.js',
+ 35 => '_inc/blocks/tiled-gallery/view.css',
+ 36 => '_inc/blocks/editor.css',
+ 37 => '_inc/blocks/vendors~swiper.59b62a96313990494c44.rtl.css',
+ 38 => '_inc/blocks/editor.rtl.css',
+ 39 => '_inc/blocks/vendors~map/mapbox-gl.f81f5e1d3c950198407d.css',
+ 40 => '_inc/blocks/vendors~map/mapbox-gl.f81f5e1d3c950198407d.js',
+ 41 => '_inc/blocks/vendors~map/mapbox-gl.f81f5e1d3c950198407d.rtl.css',
+ 42 => '_inc/blocks/vendors~swiper.59b62a96313990494c44.css',
+ 43 => '_inc/blocks/repeat-visitor/view.js',
+ 44 => '_inc/blocks/editor-beta.js',
+ 45 => '_inc/blocks/mailchimp/view.rtl.css',
+ 46 => '_inc/blocks/mailchimp/view.js',
+ 47 => '_inc/blocks/mailchimp/view.css',
+ 48 => '_inc/blocks/membership-button/view.rtl.css',
+ 49 => '_inc/blocks/membership-button/view.js',
+ 50 => '_inc/blocks/membership-button/view.css',
+ 51 => '_inc/blocks/editor.js',
+ 52 => '_inc/blocks/map/view.rtl.css',
+ 53 => '_inc/blocks/map/view.js',
+ 54 => '_inc/blocks/map/view.css',
+ 55 => '_inc/blocks/editor-beta.css',
+ 56 => '_inc/blocks/contact-info/view.rtl.css',
+ 57 => '_inc/blocks/contact-info/view.js',
+ 58 => '_inc/blocks/contact-info/view.css',
+ 59 => '_inc/blocks/slideshow/view.rtl.css',
+ 60 => '_inc/blocks/slideshow/view.js',
+ 61 => '_inc/blocks/slideshow/view.css',
+ 62 => '_inc/blocks/editor-beta.rtl.css',
+ 63 => '_inc/blocks/gif/view.rtl.css',
+ 64 => '_inc/blocks/gif/view.js',
+ 65 => '_inc/blocks/gif/view.css',
+ 66 => '_inc/idc-notice.js',
+ 67 => '_inc/jetpack-modules.js',
+ 68 => '_inc/jquery.jetpack-resize.js',
+ 69 => '_inc/genericons/genericons/genericons.css',
+ 70 => '_inc/genericons/genericons/rtl/genericons-rtl.css',
+ 71 => '_inc/genericons/genericons.css',
+ 72 => '_inc/jetpack-modules.views.js',
+ 73 => '_inc/jquery.spin.js',
+ 74 => '_inc/facebook-embed.js',
+ 75 => '_inc/twitter-timeline.js',
+ 76 => '_inc/spin.js',
+ 77 => '_inc/accessible-focus.js',
+ 78 => '_inc/jetpack-jitm.js',
+ 79 => '_inc/jetpack-modules.models.js',
+ 80 => '_inc/social-logos/social-logos.min.css',
+ 81 => '_inc/social-logos/social-logos.css',
+ 82 => '_inc/gallery-settings.js',
+ 83 => '_inc/lib/tracks/tracks-callables.js',
+ 84 => '_inc/lib/tracks/tracks-ajax.js',
+ 85 => '_inc/build/infinite-scroll/infinity.min.js',
+ 86 => '_inc/build/videopress/js/videopress-plupload.min.js',
+ 87 => '_inc/build/videopress/js/videopress-uploader.min.js',
+ 88 => '_inc/build/videopress/js/media-video-widget-extensions.min.js',
+ 89 => '_inc/build/videopress/js/editor-view.min.js',
+ 90 => '_inc/build/tiled-gallery/tiled-gallery/tiled-gallery.min.js',
+ 91 => '_inc/build/jetpack-connection-banner.min.js',
+ 92 => '_inc/build/style.min.css',
+ 93 => '_inc/build/masterbar/tracks-events.min.js',
+ 94 => '_inc/build/sharedaddy/admin-sharing.min.js',
+ 95 => '_inc/build/sharedaddy/sharing.min.js',
+ 96 => '_inc/build/jquery.spin.min.js',
+ 97 => '_inc/build/custom-post-types/comics/comics.min.js',
+ 98 => '_inc/build/custom-post-types/js/nova-drag-drop.min.js',
+ 99 => '_inc/build/custom-post-types/js/many-items.min.js',
+ 100 => '_inc/build/custom-post-types/js/menu-checkboxes.min.js',
+ 101 => '_inc/build/jquery.jetpack-resize.min.js',
+ 102 => '_inc/build/likes/post-count-jetpack.min.js',
+ 103 => '_inc/build/likes/queuehandler.min.js',
+ 104 => '_inc/build/likes/post-count.min.js',
+ 105 => '_inc/build/admin.dops-style.css',
+ 106 => '_inc/build/comment-likes/comment-like-count.min.js',
+ 107 => '_inc/build/idc-notice.min.js',
+ 108 => '_inc/build/accessible-focus.min.js',
+ 109 => '_inc/build/contact-form/js/grunion.min.js',
+ 110 => '_inc/build/contact-form/js/tinymce-plugin-form-button.min.js',
+ 111 => '_inc/build/contact-form/js/grunion-admin.min.js',
+ 112 => '_inc/build/contact-form/js/grunion-frontend.min.js',
+ 113 => '_inc/build/contact-form/js/editor-view.min.js',
+ 114 => '_inc/build/jetpack-modules.models.min.js',
+ 115 => '_inc/build/related-posts/related-posts.min.js',
+ 116 => '_inc/build/related-posts/related-posts-customizer.min.js',
+ 117 => '_inc/build/carousel/jetpack-carousel.min.js',
+ 118 => '_inc/build/shortcodes/js/brightcove.min.js',
+ 119 => '_inc/build/shortcodes/js/gist.min.js',
+ 120 => '_inc/build/shortcodes/js/recipes-printthis.min.js',
+ 121 => '_inc/build/shortcodes/js/main.min.js',
+ 122 => '_inc/build/shortcodes/js/recipes.min.js',
+ 123 => '_inc/build/shortcodes/js/jmpress.min.js',
+ 124 => '_inc/build/shortcodes/js/instagram.min.js',
+ 125 => '_inc/build/shortcodes/js/slideshow-shortcode.min.js',
+ 126 => '_inc/build/shortcodes/js/quiz.min.js',
+ 127 => '_inc/build/minileven/theme/pub/minileven/js/small-menu.min.js',
+ 128 => '_inc/build/custom-css/custom-css/js/core-customizer-css-preview.min.js',
+ 129 => '_inc/build/custom-css/custom-css/js/core-customizer-css.core-4.9.min.js',
+ 130 => '_inc/build/custom-css/custom-css/js/use-codemirror.min.js',
+ 131 => '_inc/build/custom-css/custom-css/js/core-customizer-css.min.js',
+ 132 => '_inc/build/custom-css/custom-css/js/css-editor.min.js',
+ 133 => '_inc/build/jetpack-jitm.min.js',
+ 134 => '_inc/build/gallery-settings.min.js',
+ 135 => '_inc/build/spin.min.js',
+ 136 => '_inc/build/jetpack-admin.min.js',
+ 137 => '_inc/build/admin.js',
+ 138 => '_inc/build/twitter-timeline.min.js',
+ 139 => '_inc/build/jetpack-modules.views.min.js',
+ 140 => '_inc/build/photon/photon.min.js',
+ 141 => '_inc/build/style.min.rtl.css',
+ 142 => '_inc/build/jetpack-modules.min.js',
+ 143 => '_inc/build/postmessage.min.js',
+ 144 => '_inc/build/widget-visibility/widget-conditions/widget-conditions.min.js',
+ 145 => '_inc/build/facebook-embed.min.js',
+ 146 => '_inc/build/widgets/simple-payments/customizer.min.js',
+ 147 => '_inc/build/widgets/social-icons/social-icons-admin.min.js',
+ 148 => '_inc/build/widgets/eu-cookie-law/eu-cookie-law.min.js',
+ 149 => '_inc/build/widgets/eu-cookie-law/eu-cookie-law-admin.min.js',
+ 150 => '_inc/build/widgets/gallery/js/gallery.min.js',
+ 151 => '_inc/build/widgets/gallery/js/admin.min.js',
+ 152 => '_inc/build/widgets/milestone/milestone.min.js',
+ 153 => '_inc/build/widgets/milestone/admin.min.js',
+ 154 => '_inc/build/widgets/search/js/search-widget.min.js',
+ 155 => '_inc/build/widgets/search/js/search-widget-admin.min.js',
+ 156 => '_inc/build/widgets/customizer-utils.min.js',
+ 157 => '_inc/build/widgets/contact-info/contact-info-admin.min.js',
+ 158 => '_inc/build/widgets/twitter-timeline-admin.min.js',
+ 159 => '_inc/build/widgets/google-translate/google-translate.min.js',
+ 160 => '_inc/build/lazy-images/js/lazy-images.min.js',
+ 161 => '_inc/build/admin.dops-style.rtl.css',
+ 162 => '_inc/jetpack-admin.js',
+ 163 => '_inc/jetpack-connection-banner.js',
+ 164 => '_inc/postmessage.js',
+ 165 => 'modules/infinite-scroll/infinity.js',
+ 166 => 'modules/infinite-scroll/infinity.css',
+ 167 => 'modules/infinite-scroll/themes/twentysixteen.css',
+ 168 => 'modules/infinite-scroll/themes/twentyeleven.css',
+ 169 => 'modules/infinite-scroll/themes/twentyten.css',
+ 170 => 'modules/infinite-scroll/themes/twentyseventeen-rtl.css',
+ 171 => 'modules/infinite-scroll/themes/twentyfifteen.css',
+ 172 => 'modules/infinite-scroll/themes/twentysixteen-rtl.css',
+ 173 => 'modules/infinite-scroll/themes/twentyseventeen.css',
+ 174 => 'modules/infinite-scroll/themes/twentytwelve.css',
+ 175 => 'modules/infinite-scroll/themes/twentyfourteen.css',
+ 176 => 'modules/infinite-scroll/themes/twentyfifteen-rtl.css',
+ 177 => 'modules/infinite-scroll/themes/twentythirteen.css',
+ 178 => 'modules/videopress/css/videopress-editor-style-rtl.css',
+ 179 => 'modules/videopress/css/videopress-editor-style.min.css',
+ 180 => 'modules/videopress/css/editor.css',
+ 181 => 'modules/videopress/css/videopress-editor-style-rtl.min.css',
+ 182 => 'modules/videopress/css/editor.min.css',
+ 183 => 'modules/videopress/css/editor-rtl.css',
+ 184 => 'modules/videopress/css/editor-rtl.min.css',
+ 185 => 'modules/videopress/css/videopress-editor-style.css',
+ 186 => 'modules/videopress/js/media-video-widget-extensions.js',
+ 187 => 'modules/videopress/js/videopress-plupload.js',
+ 188 => 'modules/videopress/js/videopress-uploader.js',
+ 189 => 'modules/videopress/js/editor-view.js',
+ 190 => 'modules/videopress/videopress-admin.css',
+ 191 => 'modules/videopress/videopress-admin.min.css',
+ 192 => 'modules/videopress/videopress-admin-rtl.css',
+ 193 => 'modules/videopress/videopress-admin-rtl.min.css',
+ 194 => 'modules/tiled-gallery/tiled-gallery/tiled-gallery-rtl.css',
+ 195 => 'modules/tiled-gallery/tiled-gallery/tiled-gallery.css',
+ 196 => 'modules/tiled-gallery/tiled-gallery/rtl/tiled-gallery-rtl.css',
+ 197 => 'modules/tiled-gallery/tiled-gallery/tiled-gallery.js',
+ 198 => 'modules/simple-payments/simple-payments.css',
+ 199 => 'modules/simple-payments/paypal-express-checkout.js',
+ 200 => 'modules/masterbar/overrides.css',
+ 201 => 'modules/masterbar/tracks-events.js',
+ 202 => 'modules/sharedaddy/admin-sharing-rtl.min.css',
+ 203 => 'modules/sharedaddy/sharing.js',
+ 204 => 'modules/sharedaddy/sharing.css',
+ 205 => 'modules/sharedaddy/admin-sharing.js',
+ 206 => 'modules/sharedaddy/admin-sharing.min.css',
+ 207 => 'modules/sharedaddy/admin-sharing-rtl.css',
+ 208 => 'modules/sharedaddy/admin-sharing.css',
+ 209 => 'modules/custom-post-types/comics/comics.min.css',
+ 210 => 'modules/custom-post-types/comics/comics-rtl.css',
+ 211 => 'modules/custom-post-types/comics/comics.css',
+ 212 => 'modules/custom-post-types/comics/comics-rtl.min.css',
+ 213 => 'modules/custom-post-types/comics/comics.js',
+ 214 => 'modules/custom-post-types/comics/admin.css',
+ 215 => 'modules/custom-post-types/comics/rtl/comics-rtl.css',
+ 216 => 'modules/custom-post-types/css/portfolio-shortcode.css',
+ 217 => 'modules/custom-post-types/css/testimonial-shortcode.css',
+ 218 => 'modules/custom-post-types/css/nova-font.css',
+ 219 => 'modules/custom-post-types/css/edit-items.css',
+ 220 => 'modules/custom-post-types/css/nova.css',
+ 221 => 'modules/custom-post-types/css/many-items.css',
+ 222 => 'modules/custom-post-types/js/menu-checkboxes.js',
+ 223 => 'modules/custom-post-types/js/many-items.js',
+ 224 => 'modules/custom-post-types/js/nova-drag-drop.js',
+ 225 => 'modules/calypsoify/style-gutenberg-rtl.min.css',
+ 226 => 'modules/calypsoify/style-rtl.min.css',
+ 227 => 'modules/calypsoify/style.min.css',
+ 228 => 'modules/calypsoify/style-gutenberg.min.css',
+ 229 => 'modules/calypsoify/mods-gutenberg.js',
+ 230 => 'modules/calypsoify/mods.js',
+ 231 => 'modules/likes/post-count.js',
+ 232 => 'modules/likes/post-count-jetpack.js',
+ 233 => 'modules/likes/style.css',
+ 234 => 'modules/likes/queuehandler.js',
+ 235 => 'modules/protect/protect-dashboard-widget-rtl.min.css',
+ 236 => 'modules/protect/protect-dashboard-widget.css',
+ 237 => 'modules/protect/protect-dashboard-widget.min.css',
+ 238 => 'modules/protect/protect-dashboard-widget-rtl.css',
+ 239 => 'modules/comment-likes/comment-like-count.js',
+ 240 => 'modules/comment-likes/admin-style.css',
+ 241 => 'modules/contact-form/css/editor-ui.min.css',
+ 242 => 'modules/contact-form/css/editor-ui-rtl.css',
+ 243 => 'modules/contact-form/css/editor-inline-editing-style.css',
+ 244 => 'modules/contact-form/css/editor-inline-editing-style-rtl.css',
+ 245 => 'modules/contact-form/css/editor-inline-editing-style.min.css',
+ 246 => 'modules/contact-form/css/editor-style-rtl.min.css',
+ 247 => 'modules/contact-form/css/jquery-ui-datepicker.css',
+ 248 => 'modules/contact-form/css/grunion.css',
+ 249 => 'modules/contact-form/css/editor-ui.css',
+ 250 => 'modules/contact-form/css/editor-style.css',
+ 251 => 'modules/contact-form/css/editor-style-rtl.css',
+ 252 => 'modules/contact-form/css/editor-inline-editing-style-rtl.min.css',
+ 253 => 'modules/contact-form/css/editor-style.min.css',
+ 254 => 'modules/contact-form/css/grunion-rtl.css',
+ 255 => 'modules/contact-form/css/editor-ui-rtl.min.css',
+ 256 => 'modules/contact-form/js/grunion.js',
+ 257 => 'modules/contact-form/js/grunion-admin.js',
+ 258 => 'modules/contact-form/js/editor-view.js',
+ 259 => 'modules/contact-form/js/grunion-frontend.js',
+ 260 => 'modules/contact-form/js/tinymce-plugin-form-button.js',
+ 261 => 'modules/related-posts/related-posts.css',
+ 262 => 'modules/related-posts/related-posts.js',
+ 263 => 'modules/related-posts/related-posts-rtl.css',
+ 264 => 'modules/related-posts/rtl/related-posts-rtl.css',
+ 265 => 'modules/related-posts/related-posts-customizer.js',
+ 266 => 'modules/carousel/jetpack-carousel.js',
+ 267 => 'modules/carousel/rtl/jetpack-carousel-rtl.css',
+ 268 => 'modules/carousel/jetpack-carousel.css',
+ 269 => 'modules/carousel/jetpack-carousel-rtl.css',
+ 270 => 'modules/shortcodes/css/recipes.css',
+ 271 => 'modules/shortcodes/css/slideshow-shortcode.min.css',
+ 272 => 'modules/shortcodes/css/slideshow-shortcode-rtl.css',
+ 273 => 'modules/shortcodes/css/recipes-print.min.css',
+ 274 => 'modules/shortcodes/css/recipes-rtl.css',
+ 275 => 'modules/shortcodes/css/recipes.min.css',
+ 276 => 'modules/shortcodes/css/recipes-print-rtl.css',
+ 277 => 'modules/shortcodes/css/recipes-print.css',
+ 278 => 'modules/shortcodes/css/slideshow-shortcode-rtl.min.css',
+ 279 => 'modules/shortcodes/css/recipes-rtl.min.css',
+ 280 => 'modules/shortcodes/css/recipes-print-rtl.min.css',
+ 281 => 'modules/shortcodes/css/style.css',
+ 282 => 'modules/shortcodes/css/quiz.css',
+ 283 => 'modules/shortcodes/css/slideshow-shortcode.css',
+ 284 => 'modules/shortcodes/js/brightcove.js',
+ 285 => 'modules/shortcodes/js/quiz.js',
+ 286 => 'modules/shortcodes/js/recipes-printthis.js',
+ 287 => 'modules/shortcodes/js/jmpress.js',
+ 288 => 'modules/shortcodes/js/slideshow-shortcode.js',
+ 289 => 'modules/shortcodes/js/main.js',
+ 290 => 'modules/shortcodes/js/jquery.cycle.min.js',
+ 291 => 'modules/shortcodes/js/instagram.js',
+ 292 => 'modules/shortcodes/js/recipes.js',
+ 293 => 'modules/shortcodes/js/gist.js',
+ 294 => 'modules/subscriptions/subscriptions.css',
+ 295 => 'modules/minileven/theme/pub/minileven/js/small-menu.js',
+ 296 => 'modules/minileven/theme/pub/minileven/style.css',
+ 297 => 'modules/minileven/theme/pub/minileven/rtl.css',
+ 298 => 'modules/wordads/css/style.css',
+ 299 => 'modules/custom-css/csstidy/cssparse-rtl.min.css',
+ 300 => 'modules/custom-css/csstidy/cssparse-rtl.css',
+ 301 => 'modules/custom-css/csstidy/cssparse.min.css',
+ 302 => 'modules/custom-css/csstidy/cssparsed-rtl.min.css',
+ 303 => 'modules/custom-css/csstidy/cssparsed.css',
+ 304 => 'modules/custom-css/csstidy/cssparse.css',
+ 305 => 'modules/custom-css/csstidy/cssparsed-rtl.css',
+ 306 => 'modules/custom-css/csstidy/cssparsed.min.css',
+ 307 => 'modules/custom-css/custom-css/css/blank.css',
+ 308 => 'modules/custom-css/custom-css/css/css-editor.css',
+ 309 => 'modules/custom-css/custom-css/css/use-codemirror.css',
+ 310 => 'modules/custom-css/custom-css/css/codemirror.css',
+ 311 => 'modules/custom-css/custom-css/css/codemirror-rtl.min.css',
+ 312 => 'modules/custom-css/custom-css/css/css-editor-rtl.min.css',
+ 313 => 'modules/custom-css/custom-css/css/css-editor-rtl.css',
+ 314 => 'modules/custom-css/custom-css/css/customizer-control.css',
+ 315 => 'modules/custom-css/custom-css/css/codemirror-rtl.css',
+ 316 => 'modules/custom-css/custom-css/css/css-editor.min.css',
+ 317 => 'modules/custom-css/custom-css/css/codemirror.min.css',
+ 318 => 'modules/custom-css/custom-css/css/rtl/codemirror-rtl.css',
+ 319 => 'modules/custom-css/custom-css/css/use-codemirror-rtl.css',
+ 320 => 'modules/custom-css/custom-css/css/use-codemirror.min.css',
+ 321 => 'modules/custom-css/custom-css/css/use-codemirror-rtl.min.css',
+ 322 => 'modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js',
+ 323 => 'modules/custom-css/custom-css/js/css-editor.js',
+ 324 => 'modules/custom-css/custom-css/js/codemirror.min.js',
+ 325 => 'modules/custom-css/custom-css/js/core-customizer-css-preview.js',
+ 326 => 'modules/custom-css/custom-css/js/core-customizer-css.js',
+ 327 => 'modules/custom-css/custom-css/js/use-codemirror.js',
+ 328 => 'modules/sso/jetpack-sso-login.css',
+ 329 => 'modules/sso/jetpack-sso-login.js',
+ 330 => 'modules/sso/jetpack-sso-login-rtl.css',
+ 331 => 'modules/sso/jetpack-sso-login.min.css',
+ 332 => 'modules/sso/jetpack-sso-login-rtl.min.css',
+ 333 => 'modules/theme-tools/site-logo/css/site-logo-control.css',
+ 334 => 'modules/theme-tools/site-logo/css/site-logo-control-rtl.min.css',
+ 335 => 'modules/theme-tools/site-logo/css/site-logo-control.min.css',
+ 336 => 'modules/theme-tools/site-logo/css/site-logo-control-rtl.css',
+ 337 => 'modules/theme-tools/site-logo/js/site-logo.min.js',
+ 338 => 'modules/theme-tools/site-logo/js/site-logo.js',
+ 339 => 'modules/theme-tools/site-logo/js/site-logo-header-text.js',
+ 340 => 'modules/theme-tools/site-logo/js/site-logo-control.js',
+ 341 => 'modules/theme-tools/site-logo/js/site-logo-header-text.min.js',
+ 342 => 'modules/theme-tools/site-logo/js/site-logo-control.min.js',
+ 343 => 'modules/theme-tools/compat/twentysixteen.css',
+ 344 => 'modules/theme-tools/compat/twentynineteen.css',
+ 345 => 'modules/theme-tools/compat/twentyfourteen-rtl.css',
+ 346 => 'modules/theme-tools/compat/twentyfifteen.css',
+ 347 => 'modules/theme-tools/compat/twentysixteen-rtl.css',
+ 348 => 'modules/theme-tools/compat/twentynineteen-rtl.css',
+ 349 => 'modules/theme-tools/compat/twentyfourteen.css',
+ 350 => 'modules/theme-tools/compat/twentyfifteen-rtl.css',
+ 351 => 'modules/theme-tools/content-options/customizer.js',
+ 352 => 'modules/theme-tools/js/suggest.js',
+ 353 => 'modules/theme-tools/social-menu/social-menu.css',
+ 354 => 'modules/theme-tools/responsive-videos/responsive-videos.css',
+ 355 => 'modules/theme-tools/responsive-videos/responsive-videos.js',
+ 356 => 'modules/theme-tools/responsive-videos/responsive-videos.min.js',
+ 357 => 'modules/post-by-email/post-by-email-rtl.css',
+ 358 => 'modules/post-by-email/post-by-email.min.css',
+ 359 => 'modules/post-by-email/post-by-email-rtl.min.css',
+ 360 => 'modules/post-by-email/post-by-email.css',
+ 361 => 'modules/post-by-email/post-by-email.js',
+ 362 => 'modules/photon/photon.js',
+ 363 => 'modules/plugin-search/plugin-search.css',
+ 364 => 'modules/plugin-search/plugin-search.js',
+ 365 => 'modules/widget-visibility/widget-conditions/widget-conditions-rtl.min.css',
+ 366 => 'modules/widget-visibility/widget-conditions/widget-conditions-rtl.css',
+ 367 => 'modules/widget-visibility/widget-conditions/widget-conditions.min.css',
+ 368 => 'modules/widget-visibility/widget-conditions/widget-conditions.css',
+ 369 => 'modules/widget-visibility/widget-conditions/rtl/widget-conditions-rtl.css',
+ 370 => 'modules/widget-visibility/widget-conditions/widget-conditions.js',
+ 371 => 'modules/widgets/simple-payments/customizer.css',
+ 372 => 'modules/widgets/simple-payments/customizer.js',
+ 373 => 'modules/widgets/simple-payments/style.css',
+ 374 => 'modules/widgets/twitter-timeline-admin.js',
+ 375 => 'modules/widgets/facebook-likebox/style.css',
+ 376 => 'modules/widgets/customizer-utils.js',
+ 377 => 'modules/widgets/goodreads/css/goodreads.css',
+ 378 => 'modules/widgets/goodreads/css/rtl/goodreads-rtl.css',
+ 379 => 'modules/widgets/social-media-icons/style.css',
+ 380 => 'modules/widgets/my-community/style.css',
+ 381 => 'modules/widgets/authors/style.css',
+ 382 => 'modules/widgets/social-icons/social-icons-admin.js',
+ 383 => 'modules/widgets/social-icons/social-icons-admin.css',
+ 384 => 'modules/widgets/social-icons/social-icons.css',
+ 385 => 'modules/widgets/eu-cookie-law/eu-cookie-law.js',
+ 386 => 'modules/widgets/eu-cookie-law/style.css',
+ 387 => 'modules/widgets/eu-cookie-law/eu-cookie-law-admin.js',
+ 388 => 'modules/widgets/flickr/style.css',
+ 389 => 'modules/widgets/gallery/css/admin-rtl.min.css',
+ 390 => 'modules/widgets/gallery/css/admin.css',
+ 391 => 'modules/widgets/gallery/css/admin-rtl.css',
+ 392 => 'modules/widgets/gallery/css/admin.min.css',
+ 393 => 'modules/widgets/gallery/css/rtl/admin-rtl.css',
+ 394 => 'modules/widgets/gallery/js/gallery.js',
+ 395 => 'modules/widgets/gallery/js/admin.js',
+ 396 => 'modules/widgets/top-posts/style.css',
+ 397 => 'modules/widgets/milestone/style-admin.css',
+ 398 => 'modules/widgets/milestone/milestone.js',
+ 399 => 'modules/widgets/milestone/admin.js',
+ 400 => 'modules/widgets/search/css/search-widget-frontend.css',
+ 401 => 'modules/widgets/search/css/search-widget-admin-ui.css',
+ 402 => 'modules/widgets/search/js/search-widget-admin.js',
+ 403 => 'modules/widgets/search/js/search-widget.js',
+ 404 => 'modules/widgets/contact-info/contact-info-admin.js',
+ 405 => 'modules/widgets/contact-info/contact-info-map.css',
+ 406 => 'modules/widgets/image-widget/style.css',
+ 407 => 'modules/widgets/customizer-controls.css',
+ 408 => 'modules/widgets/google-translate/google-translate.js',
+ 409 => 'modules/widgets/wordpress-post-widget/style.css',
+ 410 => 'modules/widgets/gravatar-profile.css',
+ 411 => 'modules/lazy-images/js/lazy-images.js',
+ 412 => 'modules/wpgroho.js',
+);
diff --git a/plugins/jetpack/modules/photon.php b/plugins/jetpack/modules/photon.php
new file mode 100644
index 00000000..cd61ee70
--- /dev/null
+++ b/plugins/jetpack/modules/photon.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Module Name: Image CDN
+ * Module Description: Mirrors and serves your images from our free and fast image CDN, improving your site’s performance with no additional load on your servers.
+ * Jumpstart Description: Mirrors and serves your images from our free and fast image CDN, improving your site’s performance with no additional load on your servers.
+ * Sort Order: 25
+ * Recommendation Order: 1
+ * First Introduced: 2.0
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Photos and Videos, Appearance, Recommended
+ * Feature: Recommended, Jumpstart, Appearance
+ * Additional Search Queries: photon, photo cdn, image cdn, speed, compression, resize, responsive images, responsive, content distribution network, optimize, page speed, image optimize, photon jetpack
+ */
+
+Jetpack::dns_prefetch( array(
+ '//i0.wp.com',
+ '//i1.wp.com',
+ '//i2.wp.com',
+) );
+
+Jetpack_Photon::instance();
diff --git a/plugins/jetpack/modules/photon/photon.js b/plugins/jetpack/modules/photon/photon.js
new file mode 100644
index 00000000..f7d32dfc
--- /dev/null
+++ b/plugins/jetpack/modules/photon/photon.js
@@ -0,0 +1,61 @@
+/* jshint onevar: false */
+
+( function() {
+ function recalculate() {
+ if ( this.complete ) {
+ // Support for lazy loading: if there is a lazy-src attribute and it's value
+ // is not the same as the current src we should wait until the image load event
+ var lazySrc = this.getAttribute('data-lazy-src');
+ if ( lazySrc && this.src !== lazySrc ) {
+ this.addEventListener( 'onload', recalculate );
+ return;
+ }
+
+ // Copying CSS width/height into element attributes.
+ var width = this.width;
+ var height = this.height;
+ if ( width && width > 0 && height && height > 0 ) {
+ this.setAttribute('width', width);
+ this.setAttribute('height', height);
+
+ reset_for_retina( this );
+ }
+ }
+ else {
+ this.addEventListener( 'onload', recalculate );
+ return;
+ }
+ }
+
+ /**
+ * For images lacking explicit dimensions and needing them, try to add them.
+ */
+ var restore_dims = function() {
+ var elements = document.querySelectorAll( 'img[data-recalc-dims]' );
+ for (var i = 0; i < elements.length; i++) {
+ recalculate.call( elements[i] );
+ }
+ },
+
+ /**
+ * Modify given image's markup so that devicepx-jetpack.js will act on the image and it won't be reprocessed by this script.
+ */
+ reset_for_retina = function( img ) {
+ img.removeAttribute( 'data-recalc-dims' );
+ img.removeAttribute( 'scale' );
+ };
+
+ /**
+ * Check both when page loads, and when IS is triggered.
+ */
+ if ( typeof window !== 'undefined' && typeof document !== 'undefined' ) {
+ // `DOMContentLoaded` may fire before the script has a chance to run
+ if ( document.readyState === 'loading' ) {
+ document.addEventListener( 'DOMContentLoaded', restore_dims );
+ } else {
+ restore_dims();
+ }
+ }
+
+ document.body.addEventListener( 'post-load', restore_dims );
+} )();
diff --git a/plugins/jetpack/modules/plugin-search.php b/plugins/jetpack/modules/plugin-search.php
new file mode 100644
index 00000000..b85e0586
--- /dev/null
+++ b/plugins/jetpack/modules/plugin-search.php
@@ -0,0 +1,602 @@
+<?php
+/**
+ * Disable direct access and execution.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+
+if (
+ is_admin() &&
+ Jetpack::is_active() &&
+ /** This filter is documented in _inc/lib/admin-pages/class.jetpack-react-page.php */
+ apply_filters( 'jetpack_show_promotions', true ) &&
+ jetpack_is_psh_active()
+) {
+ Jetpack_Plugin_Search::init();
+}
+
+// Register endpoints when WP REST API is initialized.
+add_action( 'rest_api_init', array( 'Jetpack_Plugin_Search', 'register_endpoints' ) );
+
+/**
+ * Class that includes cards in the plugin search results when users enter terms that match some Jetpack feature.
+ * Card can be dismissed and includes a title, description, button to enable the feature and a link for more information.
+ *
+ * @since 7.1.0
+ */
+class Jetpack_Plugin_Search {
+
+ static $slug = 'jetpack-plugin-search';
+
+ public static function init() {
+ static $instance = null;
+
+ if ( ! $instance ) {
+ jetpack_require_lib( 'tracks/client' );
+ $instance = new Jetpack_Plugin_Search();
+ }
+
+ return $instance;
+ }
+
+ public function __construct() {
+ add_action( 'current_screen', array( $this, 'start' ) );
+ }
+
+ /**
+ * Add actions and filters only if this is the plugin installation screen and it's the first page.
+ *
+ * @param object $screen
+ *
+ * @since 7.1.0
+ */
+ public function start( $screen ) {
+ if ( 'plugin-install' === $screen->base && ( ! isset( $_GET['paged'] ) || 1 == $_GET['paged'] ) ) {
+ add_action( 'admin_enqueue_scripts', array( $this, 'load_plugins_search_script' ) );
+ add_filter( 'plugins_api_result', array( $this, 'inject_jetpack_module_suggestion' ), 10, 3 );
+ add_filter( 'self_admin_url', array( $this, 'plugin_details' ) );
+ add_filter( 'plugin_install_action_links', array( $this, 'insert_module_related_links' ), 10, 2 );
+ }
+ }
+
+ /**
+ * Modify URL used to fetch to plugin information so it pulls Jetpack plugin page.
+ *
+ * @param string $url URL to load in dialog pulling the plugin page from wporg.
+ *
+ * @since 7.1.0
+ *
+ * @return string The URL with 'jetpack' instead of 'jetpack-plugin-search'.
+ */
+ public function plugin_details( $url ) {
+ return false !== stripos( $url, 'tab=plugin-information&amp;plugin=' . self::$slug )
+ ? 'plugin-install.php?tab=plugin-information&amp;plugin=jetpack&amp;TB_iframe=true&amp;width=600&amp;height=550'
+ : $url;
+ }
+
+ /**
+ * Register REST API endpoints.
+ *
+ * @since 7.1.0
+ */
+ public static function register_endpoints() {
+ register_rest_route( 'jetpack/v4', '/hints', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::dismiss',
+ 'permission_callback' => __CLASS__ . '::can_request',
+ 'args' => array(
+ 'hint' => array(
+ 'default' => '',
+ 'type' => 'string',
+ 'required' => true,
+ 'validate_callback' => __CLASS__ . '::is_hint_id',
+ ),
+ )
+ ) );
+ }
+
+ /**
+ * A WordPress REST API permission callback method that accepts a request object and
+ * decides if the current user has enough privileges to act.
+ *
+ * @since 7.1.0
+ *
+ * @return bool does a current user have enough privileges.
+ */
+ public static function can_request() {
+ return current_user_can( 'jetpack_admin_page' );
+ }
+
+ /**
+ * Validates that the ID of the hint to dismiss is a string.
+ *
+ * @since 7.1.0
+ *
+ * @param string|bool $value Value to check.
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ * @param string $param Name of the parameter passed to endpoint holding $value.
+ *
+ * @return bool|WP_Error
+ */
+ public static function is_hint_id( $value, $request, $param ) {
+ return in_array( $value, Jetpack::get_available_modules(), true )
+ ? true
+ : new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an alphanumeric string.', 'jetpack' ), $param ) );
+ }
+
+ /**
+ * A WordPress REST API callback method that accepts a request object and decides what to do with it.
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $hint Slug of card to dismiss.
+ * }
+ *
+ * @since 7.1.0
+ *
+ * @return bool|array|WP_Error a resulting value or object, or an error.
+ */
+ public static function dismiss( WP_REST_Request $request ) {
+ return self::add_to_dismissed_hints( $request['hint'] )
+ ? rest_ensure_response( array( 'code' => 'success' ) )
+ : new WP_Error( 'not_dismissed', esc_html__( 'The card could not be dismissed', 'jetpack' ), array( 'status' => 400 ) );
+ }
+
+ /**
+ * Returns a list of previously dismissed hints.
+ *
+ * @since 7.1.0
+ *
+ * @return array List of dismissed hints.
+ */
+ protected static function get_dismissed_hints() {
+ $dismissed_hints = Jetpack_Options::get_option( 'dismissed_hints' );
+ return isset( $dismissed_hints ) && is_array( $dismissed_hints )
+ ? $dismissed_hints
+ : array();
+ }
+
+ /**
+ * Save the hint in the list of dismissed hints.
+ *
+ * @since 7.1.0
+ *
+ * @param string $hint The hint id, which is a Jetpack module slug.
+ *
+ * @return bool Whether the card was added to the list and hence dismissed.
+ */
+ protected static function add_to_dismissed_hints( $hint ) {
+ return Jetpack_Options::update_option( 'dismissed_hints', array_merge( self::get_dismissed_hints(), array( $hint ) ) );
+ }
+
+ /**
+ * Checks that the module slug passed should be displayed.
+ *
+ * A feature hint will be displayed if it has not been dismissed before or if 2 or fewer other hints have been dismissed.
+ *
+ * @since 7.2.1
+ *
+ * @param string $hint The hint id, which is a Jetpack module slug.
+ *
+ * @return bool True if $hint should be displayed.
+ */
+ protected function should_display_hint( $hint ) {
+ $dismissed_hints = $this->get_dismissed_hints();
+ // If more than 2 hints have been dismissed, then show no more.
+ if ( 2 < count( $dismissed_hints ) ) {
+ return false;
+ }
+
+ $plan = Jetpack_Plan::get();
+ if ( isset( $plan['class'] ) && ( 'free' === $plan['class'] || 'personal' === $plan['class'] ) && 'vaultpress' === $hint ) {
+ return false;
+ }
+
+ return ! in_array( $hint, $dismissed_hints, true );
+ }
+
+ public function load_plugins_search_script() {
+ wp_enqueue_script( self::$slug, plugins_url( 'modules/plugin-search/plugin-search.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), JETPACK__VERSION, true );
+ wp_localize_script(
+ self::$slug,
+ 'jetpackPluginSearch',
+ array(
+ 'nonce' => wp_create_nonce( 'wp_rest' ),
+ 'base_rest_url' => rest_url( '/jetpack/v4' ),
+ 'poweredBy' => esc_html__( 'by Jetpack (installed)', 'jetpack' ),
+ 'manageSettings' => esc_html__( 'Configure', 'jetpack' ),
+ 'activateModule' => esc_html__( 'Activate Module', 'jetpack' ),
+ 'getStarted' => esc_html__( 'Get started', 'jetpack' ),
+ 'activated' => esc_html__( 'Activated', 'jetpack' ),
+ 'activating' => esc_html__( 'Activating', 'jetpack' ),
+ 'logo' => 'https://ps.w.org/jetpack/assets/icon.svg?rev=1791404',
+ 'legend' => esc_html__(
+ 'This suggestion was made by Jetpack, the security and performance plugin already installed on your site.',
+ 'jetpack'
+ ),
+ 'supportText' => esc_html__(
+ 'Learn more about these suggestions.',
+ 'jetpack'
+ ),
+ 'supportLink' => 'https://jetpack.com/redirect/?source=plugin-hint-learn-support',
+ 'hideText' => esc_html__( 'Hide this suggestion', 'jetpack' ),
+ )
+ );
+
+ wp_enqueue_style( self::$slug, plugins_url( 'modules/plugin-search/plugin-search.css', JETPACK__PLUGIN_FILE ) );
+ }
+
+ /**
+ * Get the plugin repo's data for Jetpack to populate the fields with.
+ *
+ * @return array|mixed|object|WP_Error
+ */
+ public static function get_jetpack_plugin_data() {
+ $data = get_transient( 'jetpack_plugin_data' );
+
+ if ( false === $data || is_wp_error( $data ) ) {
+ include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' );
+ $data = plugins_api( 'plugin_information', array(
+ 'slug' => 'jetpack',
+ 'is_ssl' => is_ssl(),
+ 'fields' => array(
+ 'banners' => true,
+ 'reviews' => true,
+ 'active_installs' => true,
+ 'versions' => false,
+ 'sections' => false,
+ ),
+ ) );
+ set_transient( 'jetpack_plugin_data', $data, DAY_IN_SECONDS );
+ }
+
+ return $data;
+ }
+
+ /**
+ * Create a list with additional features for those we don't have a module, like Akismet.
+ *
+ * @since 7.1.0
+ *
+ * @return array List of features.
+ */
+ public function get_extra_features() {
+ return array(
+ 'akismet' => array(
+ 'name' => 'Akismet',
+ 'search_terms' => 'akismet, anti-spam, antispam, comments, spam, spam protection, form spam, captcha, no captcha, nocaptcha, recaptcha, phising, google',
+ 'short_description' => esc_html__( 'Keep your visitors and search engines happy by stopping comment and contact form spam with Akismet.', 'jetpack' ),
+ 'requires_connection' => true,
+ 'module' => 'akismet',
+ 'sort' => '16',
+ 'learn_more_button' => 'https://jetpack.com/features/security/spam-filtering/',
+ 'configure_url' => admin_url( 'admin.php?page=akismet-key-config' ),
+ ),
+ );
+ }
+
+ /**
+ * Intercept the plugins API response and add in an appropriate card for Jetpack
+ */
+ public function inject_jetpack_module_suggestion( $result, $action, $args ) {
+ // Looks like a search query; it's matching time
+ if ( ! empty( $args->search ) ) {
+ require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php';
+ $jetpack_modules_list = array_intersect_key(
+ array_merge( $this->get_extra_features(), Jetpack_Admin::init()->get_modules() ),
+ array_flip( array(
+ 'contact-form',
+ 'lazy-images',
+ 'monitor',
+ 'photon',
+ 'photon-cdn',
+ 'protect',
+ 'publicize',
+ 'related-posts',
+ 'sharedaddy',
+ 'akismet',
+ 'vaultpress',
+ 'videopress',
+ 'search',
+ ) )
+ );
+ uasort( $jetpack_modules_list, array( $this, 'by_sorting_option' ) );
+
+ // Record event when user searches for a term over 3 chars (less than 3 is not very useful.)
+ if ( strlen( $args->search ) >= 3 ) {
+ JetpackTracking::record_user_event( 'wpa_plugin_search_term', array( 'search_term' => $args->search ) );
+ }
+
+ // Lowercase, trim, remove punctuation/special chars, decode url, remove 'jetpack'
+ $normalized_term = $this->sanitize_search_term( $args->search );
+
+ $matching_module = null;
+
+ // Try to match a passed search term with module's search terms
+ foreach ( $jetpack_modules_list as $module_slug => $module_opts ) {
+ /*
+ * Does the site's current plan support the feature?
+ * We don't use Jetpack_Plan::supports() here because
+ * that check always returns Akismet as supported,
+ * since Akismet has a free version.
+ */
+ $current_plan = Jetpack_Plan::get();
+ $is_supported_by_plan = in_array( $module_slug, $current_plan['supports'], true );
+
+ if (
+ false !== stripos( $module_opts['search_terms'] . ', ' . $module_opts['name'], $normalized_term )
+ && $is_supported_by_plan
+ ) {
+ $matching_module = $module_slug;
+ break;
+ }
+ }
+
+ if ( isset( $matching_module ) && $this->should_display_hint( $matching_module ) ) {
+ // Record event when a matching feature is found
+ JetpackTracking::record_user_event( 'wpa_plugin_search_match_found', array( 'feature' => $matching_module ) );
+
+ $inject = (array) self::get_jetpack_plugin_data();
+ $image_url = plugins_url( 'modules/plugin-search/psh', JETPACK__PLUGIN_FILE );
+ $overrides = array(
+ 'plugin-search' => true, // Helps to determine if that an injected card.
+ 'name' => sprintf( // Supplement name/description so that they clearly indicate this was added.
+ esc_html_x( 'Jetpack: %s', 'Jetpack: Module Name', 'jetpack' ),
+ $jetpack_modules_list[ $matching_module ]['name']
+ ),
+ 'short_description' => $jetpack_modules_list[ $matching_module ]['short_description'],
+ 'requires_connection' => (bool) $jetpack_modules_list[ $matching_module ]['requires_connection'],
+ 'slug' => self::$slug,
+ 'version' => JETPACK__VERSION,
+ 'icons' => array(
+ '1x' => "$image_url-128.png",
+ '2x' => "$image_url-256.png",
+ 'svg' => "$image_url.svg",
+ ),
+ );
+
+ // Splice in the base module data
+ $inject = array_merge( $inject, $jetpack_modules_list[ $matching_module ], $overrides );
+
+ // Add it to the top of the list
+ $result->plugins = array_filter( $result->plugins, array( $this, 'filter_cards' ) );
+ array_unshift( $result->plugins, $inject );
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Remove cards for Jetpack plugins since we don't want duplicates.
+ *
+ * @since 7.1.0
+ * @since 7.2.0 Only remove Jetpack.
+ *
+ * @param array|object $plugin
+ *
+ * @return bool
+ */
+ function filter_cards( $plugin ) {
+ // Take in account that before WordPress 5.1, the list of plugins is an array of objects.
+ // With WordPress 5.1 the list of plugins is an array of arrays.
+ $slug = is_array( $plugin ) ? $plugin['slug'] : $plugin->slug;
+ return ! in_array( $slug, array( 'jetpack' ), true );
+ }
+
+ /**
+ * Take a raw search query and return something a bit more standardized and
+ * easy to work with.
+ *
+ * @param String $term The raw search term
+ * @return String A simplified/sanitized version.
+ */
+ private function sanitize_search_term( $term ) {
+ $term = strtolower( urldecode( $term ) );
+
+ // remove non-alpha/space chars.
+ $term = preg_replace( '/[^a-z ]/', '', $term );
+
+ // remove strings that don't help matches.
+ $term = trim( str_replace( array( 'jetpack', 'jp', 'free', 'wordpress' ), '', $term ) );
+
+ return $term;
+ }
+
+ /**
+ * Callback function to sort the array of modules by the sort option.
+ */
+ private function by_sorting_option( $m1, $m2 ) {
+ return $m1['sort'] - $m2['sort'];
+ }
+
+ /**
+ * Builds a URL to purchase and upgrade inserting the site fragment and the affiliate code if it exists.
+ *
+ * @param string $feature Module slug (or forged one for extra features).
+ *
+ * @since 7.1.0
+ *
+ * @return string URL to upgrade.
+ */
+ private function get_upgrade_url( $feature ) {
+ $site_raw_url = Jetpack::build_raw_urls( get_home_url() );
+ $affiliateCode = Jetpack_Affiliate::init()->get_affiliate_code();
+ $user = wp_get_current_user()->ID;
+ return "https://jetpack.com/redirect/?source=plugin-hint-upgrade-$feature&site=$site_raw_url&u=$user" .
+ ( $affiliateCode ? "&aff=$affiliateCode" : '' );
+ }
+
+ /**
+ * Modify the URL to the feature settings, for example Publicize.
+ * Sharing is included here because while we still have a page in WP Admin,
+ * we prefer to send users to Calypso.
+ *
+ * @param string $feature
+ * @param string $configure_url
+ *
+ * @return string
+ * @since 7.1.0
+ *
+ */
+ private function get_configure_url( $feature, $configure_url ) {
+ $siteFragment = Jetpack::build_raw_urls( get_home_url() );
+ switch ( $feature ) {
+ case 'sharing':
+ case 'publicize':
+ $configure_url = "https://wordpress.com/marketing/connections/$siteFragment";
+ break;
+ case 'seo-tools':
+ $configure_url = "https://wordpress.com/marketing/traffic/$siteFragment#seo";
+ break;
+ case 'google-analytics':
+ $configure_url = "https://wordpress.com/marketing/traffic/$siteFragment#analytics";
+ break;
+ case 'wordads':
+ $configure_url = "https://wordpress.com/ads/settings/$siteFragment";
+ break;
+ }
+ return $configure_url;
+ }
+
+ /**
+ * Put some more appropriate links on our custom result cards.
+ */
+ public function insert_module_related_links( $links, $plugin ) {
+ if ( self::$slug !== $plugin['slug'] ) {
+ return $links;
+ }
+
+ // By the time this filter is applied, self_admin_url was already applied and we don't need it anymore.
+ remove_filter( 'self_admin_url', array( $this, 'plugin_details' ) );
+
+ $links = array();
+
+ if ( 'akismet' === $plugin['module'] || 'vaultpress' === $plugin['module'] ) {
+ $links['jp_get_started'] = '<a
+ id="plugin-select-settings"
+ class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button"
+ href="https://jetpack.com/redirect/?source=plugin-hint-learn-' . $plugin['module'] . '"
+ data-module="' . esc_attr( $plugin['module'] ) . '"
+ data-track="get_started"
+ >' . esc_html__( 'Get started', 'jetpack' ) . '</a>';
+ // Jetpack installed, active, feature not enabled; prompt to enable.
+ } elseif (
+ current_user_can( 'jetpack_activate_modules' ) &&
+ ! Jetpack::is_module_active( $plugin['module'] ) &&
+ Jetpack_Plan::supports( $plugin['module'] )
+ ) {
+ $links[] = '<button
+ id="plugin-select-activate"
+ class="jetpack-plugin-search__primary button"
+ data-module="' . esc_attr( $plugin['module'] ) . '"
+ data-configure-url="' . esc_url( $this->get_configure_url( $plugin['module'], $plugin['configure_url'] ) ) . '"
+ > ' . esc_html__( 'Enable', 'jetpack' ) . '</button>';
+
+ // Jetpack installed, active, feature enabled; link to settings.
+ } elseif (
+ ! empty( $plugin['configure_url'] ) &&
+ current_user_can( 'jetpack_configure_modules' ) &&
+ Jetpack::is_module_active( $plugin['module'] ) &&
+ /** This filter is documented in class.jetpack-admin.php */
+ apply_filters( 'jetpack_module_configurable_' . $plugin['module'], false )
+ ) {
+ $links[] = '<a
+ id="plugin-select-settings"
+ class="jetpack-plugin-search__primary button jetpack-plugin-search__configure"
+ href="' . esc_url( $this->get_configure_url( $plugin['module'], $plugin['configure_url'] ) ) . '"
+ data-module="' . esc_attr( $plugin['module'] ) . '"
+ data-track="configure"
+ >' . esc_html__( 'Configure', 'jetpack' ) . '</a>';
+ // Module is active, doesn't have options to configure
+ } elseif ( Jetpack::is_module_active( $plugin['module'] ) ) {
+ $links['jp_get_started'] = '<a
+ id="plugin-select-settings"
+ class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button"
+ href="https://jetpack.com/redirect/?source=plugin-hint-learn-' . $plugin['module'] . '"
+ data-module="' . esc_attr( $plugin['module'] ) . '"
+ data-track="get_started"
+ >' . esc_html__( 'Get started', 'jetpack' ) . '</a>';
+ }
+
+ // Add link pointing to a relevant doc page in jetpack.com only if the Get started button isn't displayed.
+ if ( ! empty( $plugin['learn_more_button'] ) && ! isset( $links['jp_get_started'] ) ) {
+ $links[] = '<a
+ class="jetpack-plugin-search__learn-more"
+ href="' . esc_url( $plugin['learn_more_button'] ) . '"
+ target="_blank"
+ data-module="' . esc_attr( $plugin['module'] ) . '"
+ data-track="learn_more"
+ >' . esc_html__( 'Learn more', 'jetpack' ) . '</a>';
+ }
+
+ // Dismiss link
+ $links[] = '<a
+ class="jetpack-plugin-search__dismiss"
+ data-module="' . esc_attr( $plugin['module'] ) . '"
+ >' . esc_html__( 'Hide this suggestion', 'jetpack' ) . '</a>';
+
+ return $links;
+ }
+
+}
+
+/**
+ * Master control that checks if Plugin search hints is active.
+ *
+ * @since 7.1.1
+ *
+ * @return bool True if PSH is active.
+ */
+function jetpack_is_psh_active() {
+ // false means unset, 1 means active, 0 means inactive.
+ $status = get_transient( 'jetpack_psh_status' );
+
+ if ( false === $status ) {
+ $error = false;
+ $status = jetpack_get_remote_is_psh_active( $error );
+ set_transient(
+ 'jetpack_psh_status',
+ // Cache as int
+ (int) $status,
+ // If there was an error, still cache but for a shorter time
+ ( $error ? 5 : 15 ) * MINUTE_IN_SECONDS
+ );
+ }
+
+ return (bool) $status;
+}
+
+/**
+ * Makes remote request to determine if Plugin search hints is active.
+ *
+ * @since 7.1.1
+ * @internal
+ *
+ * @param bool &$error Did the remote request result in an error?
+ * @return bool True if PSH is active.
+ */
+function jetpack_get_remote_is_psh_active( &$error ) {
+ $response = wp_remote_get( 'https://jetpack.com/psh-status/' );
+ if ( is_wp_error( $response ) ) {
+ $error = true;
+ return true;
+ }
+
+ $body = wp_remote_retrieve_body( $response );
+ if ( empty( $body ) ) {
+ $error = true;
+ return true;
+ }
+
+ $json = json_decode( $body );
+ if ( ! isset( $json->active ) ) {
+ $error = true;
+ return true;
+ }
+
+ $error = false;
+ return (bool) $json->active;
+}
diff --git a/plugins/jetpack/modules/plugin-search/plugin-search.css b/plugins/jetpack/modules/plugin-search/plugin-search.css
new file mode 100644
index 00000000..8623d3f4
--- /dev/null
+++ b/plugins/jetpack/modules/plugin-search/plugin-search.css
@@ -0,0 +1,84 @@
+.plugin-card-jetpack-plugin-search h3 {
+ margin: 0 0 4px 0;
+}
+
+.plugin-card-jetpack-plugin-search .column-name,
+.plugin-card-jetpack-plugin-search .column-description {
+ margin-right: 20px;
+}
+
+.plugin-card-jetpack-plugin-search .action-links {
+ overflow: auto;
+ position: static;
+}
+
+@media screen and (max-width: 1100px) and (min-width: 782px), (max-width: 480px) {
+ .plugin-card-jetpack-plugin-search .action-links {
+ margin-left: 0;
+ }
+}
+
+.plugin-card-jetpack-plugin-search .plugin-action-buttons {
+ margin: 0;
+ width: 100%;
+ text-align: left;
+ white-space: nowrap;
+}
+
+.plugin-card-jetpack-plugin-search .plugin-action-buttons .jetpack-plugin-search__primary {
+ background: #00be28;
+ border-color: #00a523;
+ color: #fff;
+ box-shadow: 0 1px 0 #c5e2c3;
+}
+
+.plugin-card-jetpack-plugin-search .plugin-action-buttons .jetpack-plugin-search__primary:hover {
+ background: #00a523;
+ border-color: #008b1d;
+ color: #fff;
+}
+
+.plugin-card-jetpack-plugin-search .plugin-card-bottom {
+ display: none;
+}
+
+.jetpack-plugin-search__bottom {
+ display: flex;
+ align-items: center;
+ align-content: space-between;
+ clear: both;
+ padding: 12px 20px;
+ background-color: #fafafa;
+ border-top: 1px solid #ddd;
+ overflow: hidden;
+}
+
+.jetpack-plugin-search__text {
+ flex: 1;
+ margin: 0 24px 0 16px;
+}
+
+@media screen and (max-width: 1100px) and (min-width: 782px), (max-width: 480px) {
+ .plugin-card-jetpack-plugin-search .plugin-action-buttons li {
+ display: block;
+ }
+ .plugin-card-jetpack-plugin-search .plugin-action-buttons li button {
+ margin-right: 0;
+ }
+}
+
+/* Hides the link to dismiss cards when it's in action links are before being moved to bottom row*/
+.action-links .jetpack-plugin-search__dismiss {
+ display: none;
+}
+
+.jetpack-plugin-search__bottom .jetpack-plugin-search__dismiss {
+ color: #484848;
+ font-style: italic;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.jetpack-plugin-search__dismiss:hover {
+ color: #666;
+}
diff --git a/plugins/jetpack/modules/plugin-search/plugin-search.js b/plugins/jetpack/modules/plugin-search/plugin-search.js
new file mode 100644
index 00000000..1d18a438
--- /dev/null
+++ b/plugins/jetpack/modules/plugin-search/plugin-search.js
@@ -0,0 +1,273 @@
+/**
+ * Handles the activation of a Jetpack feature, dismissing the card, and replacing the bottom row
+ * of the card with customized content.
+ */
+
+/* global jetpackPluginSearch, JSON, jpTracksAJAX */
+
+var JetpackPSH = {};
+
+( function( $, jpsh ) {
+ JetpackPSH = {
+ $pluginFilter: $( '#plugin-filter' ),
+
+ /**
+ * Get parent search hint element.
+ * @returns {Element | null}
+ */
+ getCard: function() {
+ return document.querySelector( '.plugin-card-jetpack-plugin-search' );
+ },
+
+ /**
+ * Track user event such as a click on a button or a link.
+ *
+ * @param {string} eventName Event identifier.
+ * @param {object} feature Identifier of feature involved in the event.
+ * @param {object} target Object where action was performed.
+ */
+ trackEvent: function( eventName, feature, target ) {
+ jpTracksAJAX
+ .record_ajax_event( eventName, 'click', { feature: feature } )
+ .always( function() {
+ if ( 'undefined' !== typeof target && !! target.getAttribute( 'href' ) ) {
+ // If it has an href, follow it.
+ window.location = target.getAttribute( 'href' );
+ }
+ } );
+ },
+
+ /**
+ * Update title of the card to add a mention that the result is from the Jetpack plugin.
+ */
+ updateCardTitle: function() {
+ var hint = JetpackPSH.getCard();
+
+ if ( 'object' === typeof hint && null !== hint ) {
+ var title = hint.querySelector( '.column-name h3' );
+ title.outerHTML =
+ title.outerHTML + '<strong>' + jetpackPluginSearch.poweredBy + '</strong>';
+ }
+ },
+
+ /**
+ * Move action links below description.
+ */
+ moveActionLinks: function() {
+ var hint = JetpackPSH.getCard();
+ if ( 'object' === typeof hint && null !== hint ) {
+ var descriptionContainer = hint.querySelector( '.column-description' );
+ // Keep only the first paragraph. The second is the plugin author.
+ var descriptionText = descriptionContainer.querySelector( 'p:first-child' );
+ var actionLinks = hint.querySelector( '.action-links' );
+
+ // Change the contents of the description, to keep the description text and the action links.
+ descriptionContainer.innerHTML = descriptionText.outerHTML + actionLinks.outerHTML;
+
+ // Remove the action links from their default location.
+ actionLinks.parentNode.removeChild( actionLinks );
+ }
+ },
+
+ /**
+ * Replace bottom row of the card to insert logo, text and link to dismiss the card.
+ */
+ replaceCardBottom: function() {
+ var hint = JetpackPSH.getCard();
+ if ( 'object' === typeof hint && null !== hint ) {
+ hint.querySelector( '.plugin-card-bottom' ).outerHTML =
+ '<div class="jetpack-plugin-search__bottom"><img src="' +
+ jetpackPluginSearch.logo +
+ '" width="32" />' +
+ '<p class="jetpack-plugin-search__text">' +
+ jetpackPluginSearch.legend +
+ ' <a class="jetpack-plugin-search__support_link" href="' +
+ jetpackPluginSearch.supportLink +
+ '" target="_blank" rel="noopener noreferrer" data-track="support_link" >' +
+ jetpackPluginSearch.supportText +
+ '</a>' +
+ '</p>' +
+ '</div>';
+
+ // Remove link and parent li from action links and move it to bottom row
+ var dismissLink = document.querySelector( '.jetpack-plugin-search__dismiss' );
+ dismissLink.parentNode.parentNode.removeChild( dismissLink.parentNode );
+ document.querySelector( '.jetpack-plugin-search__bottom' ).appendChild( dismissLink );
+ }
+ },
+
+ /**
+ * Check if plugin card list nodes changed. If there's a Jetpack PSH card, replace the title and the bottom row.
+ * @param {array} mutationsList
+ */
+ replaceOnNewResults: function( mutationsList ) {
+ mutationsList.forEach( function( mutation ) {
+ if (
+ 'childList' === mutation.type &&
+ 1 === document.querySelectorAll( '.plugin-card-jetpack-plugin-search' ).length
+ ) {
+ JetpackPSH.updateCardTitle();
+ JetpackPSH.moveActionLinks();
+ JetpackPSH.replaceCardBottom();
+ }
+ } );
+ },
+
+ dismiss: function( moduleName ) {
+ document.getElementById( 'the-list' ).removeChild( JetpackPSH.getCard() );
+ $.ajax( {
+ url: jpsh.base_rest_url + '/hints',
+ method: 'post',
+ beforeSend: function( xhr ) {
+ xhr.setRequestHeader( 'X-WP-Nonce', jpsh.nonce );
+ },
+ data: JSON.stringify( {
+ hint: moduleName,
+ } ),
+ contentType: 'application/json',
+ dataType: 'json',
+ } ).done( function() {
+ JetpackPSH.trackEvent( 'wpa_plugin_search_dismiss', moduleName );
+ } );
+ },
+
+ ajaxActivateModule: function( moduleName ) {
+ var $moduleBtn = JetpackPSH.$pluginFilter.find( '#plugin-select-activate' );
+ $moduleBtn.toggleClass( 'install-now updating-message' );
+ $moduleBtn.prop( 'disabled', true );
+ $moduleBtn.text( jpsh.activating );
+ var data = {};
+ data[ moduleName ] = true;
+ $.ajax( {
+ url: jpsh.base_rest_url + '/settings',
+ method: 'post',
+ beforeSend: function( xhr ) {
+ xhr.setRequestHeader( 'X-WP-Nonce', jpsh.nonce );
+ },
+ data: JSON.stringify( data ),
+ contentType: 'application/json',
+ dataType: 'json',
+ } )
+ .done( function() {
+ JetpackPSH.updateButton( moduleName );
+ JetpackPSH.trackEvent( 'wpa_plugin_search_activate', moduleName );
+ } )
+ .error( function() {
+ $moduleBtn.toggleClass( 'install-now updating-message' );
+ } );
+ },
+
+ // Remove onclick handler, disable loading spinner, update button to redirect to module settings.
+ updateButton: function( moduleName ) {
+ $.ajax( {
+ url: jpsh.base_rest_url + '/module/' + moduleName,
+ method: 'get',
+ beforeSend: function( xhr ) {
+ xhr.setRequestHeader( 'X-WP-Nonce', jpsh.nonce );
+ },
+ dataType: 'json',
+ } ).done( function( response ) {
+ var $moduleBtn = JetpackPSH.$pluginFilter.find( '#plugin-select-activate' );
+ $moduleBtn.prop( 'onclick', null ).off( 'click' );
+ $moduleBtn.toggleClass( 'install-now updating-message' );
+ $moduleBtn.text( jpsh.activated );
+ setTimeout( function() {
+ var url = 'https://jetpack.com/redirect/?source=plugin-hint-learn-' + moduleName,
+ label = jpsh.getStarted,
+ classes = 'jetpack-plugin-search__primary button',
+ track = 'configure';
+
+ // If the feature has options in Jetpack admin UI, link to them.
+ if ( response.options && 0 < Object.keys( response.options ).length ) {
+ url = $moduleBtn.data( 'configure-url' );
+ label = jpsh.manageSettings;
+ classes += ' jetpack-plugin-search__configure';
+ } else {
+ // If it has no options, the Get started button will be displayed so remove the Learn more link if it's there.
+ var learnMore = document.querySelector( '.jetpack-plugin-search__learn-more' );
+ learnMore.parentNode.removeChild( learnMore );
+ classes += ' jetpack-plugin-search__get-started';
+ track = 'get_started';
+ }
+ $moduleBtn.replaceWith(
+ '<a id="plugin-select-settings" class="' +
+ classes +
+ '" href="' +
+ url +
+ '" data-module="' +
+ moduleName +
+ '" data-track="' +
+ track +
+ '">' +
+ label +
+ '</a>'
+ );
+ }, 1000 );
+ } );
+ },
+
+ /**
+ * Start suggesting.
+ */
+ init: function() {
+ if ( JetpackPSH.$pluginFilter.length < 1 ) {
+ return;
+ }
+
+ // Update title to show that the suggestion is from Jetpack.
+ JetpackPSH.updateCardTitle();
+
+ // Update the description and action links.
+ JetpackPSH.moveActionLinks();
+
+ // Replace PSH bottom row on page load
+ JetpackPSH.replaceCardBottom();
+
+ // Listen for changes in plugin search results
+ var resultsObserver = new MutationObserver( JetpackPSH.replaceOnNewResults );
+ resultsObserver.observe( document.getElementById( 'plugin-filter' ), { childList: true } );
+
+ JetpackPSH.$pluginFilter
+ .on( 'click', '.jetpack-plugin-search__dismiss', function( event ) {
+ event.preventDefault();
+ JetpackPSH.dismiss( $( this ).data( 'module' ) );
+ } )
+ .on( 'click', 'button#plugin-select-activate', function( event ) {
+ event.preventDefault();
+ JetpackPSH.ajaxActivateModule( $( this ).data( 'module' ) );
+ } )
+ .on( 'click', '.jetpack-plugin-search__primary', function( event ) {
+ event.preventDefault();
+ var $this = $( this );
+ if ( $this.data( 'track' ) ) {
+ // This catches Purchase, Configure, and Get started. Feature activation is tracked when it ends successfully, in its callback.
+ JetpackPSH.trackEvent(
+ 'wpa_plugin_search_' + $this.data( 'track' ),
+ $this.data( 'module' ),
+ $this.get( 0 )
+ );
+ }
+ } )
+ .on( 'click', '.jetpack-plugin-search__learn-more', function( event ) {
+ event.preventDefault();
+ var $this = $( this );
+ JetpackPSH.trackEvent(
+ 'wpa_plugin_search_learn_more',
+ $this.data( 'module' ),
+ $this.get( 0 )
+ );
+ } )
+ .on( 'click', '.jetpack-plugin-search__support_link', function( event ) {
+ event.preventDefault();
+ var $this = $( this );
+ JetpackPSH.trackEvent(
+ 'wpa_plugin_search_support_link',
+ $this.data( 'module' ),
+ $this.get( 0 )
+ );
+ } );
+ },
+ };
+
+ JetpackPSH.init();
+} )( jQuery, jetpackPluginSearch );
diff --git a/plugins/jetpack/modules/plugin-search/psh-128.png b/plugins/jetpack/modules/plugin-search/psh-128.png
new file mode 100644
index 00000000..20c74c4c
--- /dev/null
+++ b/plugins/jetpack/modules/plugin-search/psh-128.png
Binary files differ
diff --git a/plugins/jetpack/modules/plugin-search/psh-256.png b/plugins/jetpack/modules/plugin-search/psh-256.png
new file mode 100644
index 00000000..2dfc2207
--- /dev/null
+++ b/plugins/jetpack/modules/plugin-search/psh-256.png
Binary files differ
diff --git a/plugins/jetpack/modules/plugin-search/psh.svg b/plugins/jetpack/modules/plugin-search/psh.svg
new file mode 100644
index 00000000..5b2609ef
--- /dev/null
+++ b/plugins/jetpack/modules/plugin-search/psh.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 183 104" xmlns="http://www.w3.org/2000/svg"><path d="m.3 99.6c11-.6 22.1-.7 33.1-.8l16.6-.1h16.6l33.1.2c11 .1 22.1.2 33.1.6.2 0 .4.2.4.4s-.2.4-.4.4c-11 .4-22.1.5-33.1.6l-33.1.2h-16.6l-16.6-.1c-11-.1-22.1-.3-33.1-.8-.1 0-.2-.1-.2-.2 0-.3.1-.4.2-.4zm152.5 0c2.5-.5 5-.7 7.5-.8l3.7-.1h3.7c2.5 0 5 .1 7.5.2s5 .2 7.5.6c.2 0 .4.2.4.5 0 .2-.2.3-.4.4-2.5.4-5 .5-7.5.6s-5 .1-7.5.2h-3.7l-3.7-.1c-2.5-.1-5-.3-7.5-.8-.1 0-.2-.1-.2-.2 0-.4.1-.5.2-.5z" fill="#e3eaf0"/><path d="m46.1 82.8v-57.8h70.1v57.8h-66.3" fill="#d8dee4"/><path d="m46.1 24.7v-13.6h98.2v13.5h-92.8" fill="#BBC9D5"/><path d="m144.3 25.2.9 57.3-28.9.1v-57.4z" fill="#ccced0"/><path d="m102.5 82.7h-48.2v-32.1h53.1v32.1m0-35.7h-53.1v-13.2h53.1z" fill="#fff"/><path d="m96.1 41.6c-.7.2-1.4.5-2.1.8-.6.4-1.3.7-1.9 1.1-.4.3-.9.6-1.4.9.4-.5.9-1.1 1.3-1.6.5-.6 1-1.3 1.4-2 .5-.7.9-1.4 1.2-2.2 0-.1 0-.1-.1-.1h-.1c-.7.5-1.3 1.1-1.8 1.7s-1 1.3-1.5 1.9c-.4.6-.9 1.2-1.3 1.9.2-.7.5-1.4.7-2.2l.6-2.4c.2-.8.3-1.6.3-2.5v-.1h-.1c-.4.7-.8 1.5-1 2.2-.3.8-.5 1.6-.7 2.3-.2.6-.3 1.2-.4 1.8 0-.7-.1-1.5-.1-2.2-.1-.9-.2-1.7-.3-2.6s-.3-1.7-.5-2.6c0-.1-.1-.2-.2-.1-.1 0-.1.1-.1.2-.1.9-.1 1.8-.1 2.6 0 .9.1 1.7.1 2.6.1.9.1 1.7.2 2.6-.2-.5-.5-1.1-.8-1.6-.3-.6-.7-1.3-1-1.9l-1.2-1.8c-.1-.1-.2-.1-.3 0-.1 0-.1.1-.1.2.2.7.4 1.4.7 2.1s.6 1.3.9 2c.2.3.3.6.5.9-.5-.5-1-1.1-1.5-1.6-.7-.6-1.3-1.3-2-1.9s-1.4-1.2-2.2-1.7c-.1-.1-.2 0-.3.1v.2c.5.8 1.1 1.5 1.7 2.1.6.7 1.3 1.3 1.9 2 .6.6 1.3 1.3 2 1.9l.1.1c-.4-.2-.8-.4-1.2-.5-.6-.2-1.2-.5-1.9-.7-.6-.2-1.3-.4-1.9-.5-.1 0-.2.1-.2.2s0 .1.1.2c.5.4 1.1.7 1.7 1s1.2.6 1.8.8c.6.3 1.2.5 1.9.7.6.2 1.2.3 1.9.4h.3c.7-.3 1.4-.6 2-.9.6-.4 1.3-.7 1.9-1.1l1.8-1.2c.6-.4 1.2-.9 1.7-1.5v-.1c-.3.1-.4.1-.4.1z" fill="#ccced0"/><path d="m147.6 24.9c-8.7.9-17.4.9-26.1 1l-26.1.3h-26.1c-8.7 0-17.4.1-26.1-.7v-.5c8.7-.9 17.4-.9 26.1-1l26.1-.3h26.1c8.7 0 17.4-.1 26.1.7z" fill="#46799A"/><path d="m77.3 70c-1.1.6-2.1 1.3-2.9 2.2l.3-1.1c.4-1.7 1-3.4 1.6-5.1l-.3-.2c-.6.7-1.1 1.5-1.4 2.4-.4.9-.7 1.7-.9 2.6s-.4 1.8-.5 2.8v.8c-.2.6-.3 1.4-.1 2h.2l.1-.1v.1h.2c.1-.5.3-.9.4-1.4.1-.1.1-.2.2-.3.3-.5.7-1 1-1.5.4-.5.8-.9 1.3-1.4s.9-.9 1.3-1.5c-.2 0-.5-.3-.5-.3zm24.6-10.4c-.5-.9-1.3-2-2.8-2.5-.6-.2-1.3-.3-2-.3.3-.4.6-.7.9-1.1l-.3-.3c-.6.5-1.1 1-1.7 1.5-.5.1-1 .3-1.4.5-.2-1-.6-2.3-1.8-3.3-.8-.7-1.9-1.1-2.9-1.2-1.1-.1-2.1.7-2.3 1.8 0 .3 0 .5.1.8.3 1.1.9 2 1.8 2.7 1 .8 2.3 1.3 3.6 1.3.3 0 .6 0 .9-.1-1 1.5-1.8 3.1-2.5 4.8l-.1.1c-.2-1-.7-2.8-2.5-3.8-1.1-.6-2.3-.9-3.6-.7-1 .1-1.7 1-1.6 2.1 0 .1 0 .3.1.4.4 1.2 1.2 2.2 2.3 2.9 1.4.8 3 1 4.5.5 0 .1.1.2.2.3-.5 1.7-.8 3.4-1 5.1-.2-.6-1.4-2.8-3.9-3.3-1.2-.2-2.5 0-3.6.5-.9.5-1.3 1.6-.8 2.5.1.1.1.2.2.3.8 1 1.9 1.7 3.2 1.9.3.1.7.1 1.1.1 1.2 0 2.3-.4 3.3-1l-.1.2.2.3c.1.2.2.3.3.5-.1 1.5 0 3.1.2 4.6h.2c.2-1.3.3-2.5.4-3.8.9.8 2.1 1.2 3.4 1.2 1.2 0 2.3-.4 3.3-1 .5-.3.8-.8.8-1.4s-.2-1.1-.6-1.5c-.7-.7-1.9-1.5-3.5-1.5-1.1 0-2.1.3-3 .9.2-1.2.4-2.4.8-3.6.9.8 2 1.3 3.1 1.4h.5c1 0 2-.3 2.9-.8.5-.3.8-.8.9-1.3.1-.6-.1-1.1-.5-1.5-.7-.7-1.8-1.6-3.3-1.7-.8-.1-1.7.1-2.5.4.4-1 .9-2 1.5-3 .1-.1.1-.2.2-.4.6.9 1.6 1.7 2.7 2 .6.2 1.2.3 1.8.3s1.1-.1 1.6-.2c.5-.2 1-.6 1.2-1.1.5-.4.4-1 .1-1.5zm-15.2 4.9c-.9-.5-1.5-1.3-1.9-2.3-.1-.4.1-.9.5-1.1h.8c.8 0 1.6.2 2.3.6 1.5.9 1.9 2.6 2 3.3-1.2.3-2.6.1-3.7-.5zm-1.5 7.7c-1-.2-1.9-.7-2.6-1.6-.3-.4-.2-.9.1-1.2l.1-.1c.7-.3 1.4-.5 2.2-.5.3 0 .5 0 .8.1 1.7.3 2.7 1.8 3 2.5-.9.8-2.3 1.1-3.6.8zm8.8-1.3c1.3 0 2.2.6 2.8 1.2.3.3.4.9 0 1.2 0 0-.1.1-.2.1-.7.6-1.6.9-2.6.9-1.6 0-2.6-.8-3.2-1.5 0-.2.1-.4.1-.6.6-.5 1.7-1.3 3.1-1.3zm1.4-6.6c1.2.1 2.2.8 2.7 1.4.2.2.2.4.2.7s-.2.5-.4.6c-.9.5-1.9.7-2.9.6-1.1-.1-2.2-.7-2.9-1.5v-.1c.1-.3.2-.7.3-1 .9-.5 1.9-.8 3-.7zm-1.7-5.6c-1.3.2-2.6-.2-3.6-1-.7-.6-1.2-1.3-1.4-2.2-.2-.6.2-1.2.7-1.3h.4c.9.1 1.7.4 2.4 1 1.4 1 1.5 2.7 1.5 3.5zm7.4 2.1c-.1.2-.3.4-.6.5-.9.3-1.9.3-2.9-.1-1-.3-1.9-1.1-2.4-2l.9-1.2c.9-.2 1.8-.2 2.6.1 1.2.4 1.9 1.3 2.3 2 .2.2.2.5.1.7z" fill="#ccced0"/><path d="m178.2 57.1c-.1-.5-.5-.9-1-.9-2.1 0-4.2-.2-6.3-.5s-4.2-.8-6.2-1.5-3.9-1.6-5.7-2.7-3.3-2.5-4.6-4.1l-.1-.1c-.4-.3-1-.3-1.3.1-1.2 1.4-2.8 2.8-4.3 4-1.6 1.2-3.3 2.2-5.1 3s-3.7 1.4-5.6 1.8-3.9.5-5.9.3c-.4 0-.7.3-.8.6-.7 4.8-.9 9.5-.6 14.3.3 4.7 1.2 9.5 2.9 13.9 1.7 4.5 4.3 8.6 7.8 11.9s7.8 5.5 12.4 6.7h.1c2.5-.1 4.9-.8 7.1-1.8s4.4-2.3 6.2-4c3.8-3.2 6.7-7.4 8.6-12 2-4.6 2.8-9.5 3.1-14.4s0-9.8-.7-14.6z" fill="#00be28"/><path d="m145.3 78.3 7.4 5.9 15.1-17.2" fill="#00be28"/><path d="m168.1 66.9c-.1-.1-.3-.2-.5-.1-1.5 1.2-2.9 2.6-4.2 3.9l-3.9 4.2c-2.3 2.6-4.6 5.1-6.9 7.7-1-.8-1.9-1.5-2.9-2.3l-1.9-1.5c-.7-.5-1.2-1.1-2.1-1.2h-.4c-.4.2-.6.6-.4 1 .3.9 1 1.2 1.6 1.8l1.8 1.5c1.2 1 2.4 2 3.7 3 .5.4 1.1.3 1.5-.1 2.5-2.9 5-5.7 7.5-8.6 1.2-1.5 2.5-2.9 3.7-4.4s2.3-3 3.3-4.7c.2.1.1-.1.1-.2z" fill="#fff"/><path d="m137.2 99.7c-5-.2-10-.4-15-.5s-10-.2-15-.2c-10-.1-19.9-.1-29.9-.1l-29.9-.1h-18.7c-.7 0-1.1 0-1.6-.1s-.9-.3-1.3-.6c-.8-.6-1.3-1.4-1.5-2.4 0-.2-.1-.5-.1-.7v-1.6l52.9-.1h4.8v2.4c0 .3.2.5.5.5h24.5c.3 0 .5-.2.5-.5v-2.7c3.5 0 6.9-.1 10.4-.2 4.5-.1 9-.2 13.5-.4.1 0 .2-.1.2-.3 0-.1-.1-.2-.2-.2-4.5-.2-9-.3-13.5-.4s-9-.2-13.5-.2c-9-.1-18-.1-27-.1l-54.2-.2c-.6 0-1.2.5-1.2 1.2v2.8c0 .4 0 .8.1 1.2.5 2.3 2.2 4.2 4.5 4.8.7.2 1.6.2 2.2.2h18.7l29.9-.1c10 0 19.9 0 29.9-.1 5 0 10-.1 15-.2s10-.2 15-.5c.1 0 .2-.1.2-.3s-.1-.3-.2-.3zm16.6-99.4-25.8-.1h-85.1c-.6 0-1.2.1-1.8.3-1.2.3-2.3.9-3.3 1.7-1.9 1.6-3.1 4-3 6.5v82.5h2.2v-82.4c0-2.9 1.9-5.4 4.6-6.1.4-.1.9-.2 1.4-.2h85.1l24.9-.1.2 19.7.1 10.3.2 10.3c0 .2.2.3.4.3.1 0 .3-.1.3-.3l.2-10.3.1-10.3.2-20.6c-.1-.7-.4-1.1-.9-1.2z" fill="#ccced0"/><path d="m54.3 78.8c13.6-5.6 42.9-1.9 52.5 4.1l-52.5-.2z" fill="#6F93AD"/></svg> \ No newline at end of file
diff --git a/plugins/jetpack/modules/post-by-email.php b/plugins/jetpack/modules/post-by-email.php
new file mode 100644
index 00000000..4d71b93a
--- /dev/null
+++ b/plugins/jetpack/modules/post-by-email.php
@@ -0,0 +1,202 @@
+<?php
+
+/**
+ * Module Name: Post by email
+ * Module Description: Publish posts by sending an email
+ * First Introduced: 2.0
+ * Sort Order: 14
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Writing
+ * Feature: Writing
+ * Additional Search Queries: post by email, email
+ */
+
+add_action( 'jetpack_modules_loaded', array( 'Jetpack_Post_By_Email', 'init' ) );
+
+Jetpack::enable_module_configurable( __FILE__ );
+
+class Jetpack_Post_By_Email {
+ public static function init() {
+ static $instance = NULL;
+
+ if ( !$instance ) {
+ $instance = new Jetpack_Post_By_Email;
+ }
+
+ return $instance;
+ }
+
+ function __construct() {
+ add_action( 'init', array( &$this, 'action_init' ) );
+ }
+
+ function action_init() {
+ if ( ! current_user_can( 'edit_posts' ) )
+ return;
+
+ add_action( 'profile_personal_options', array( &$this, 'user_profile' ) );
+ add_action( 'admin_print_scripts-profile.php', array( &$this, 'profile_scripts' ) );
+
+ add_action( 'wp_ajax_jetpack_post_by_email_enable', array( &$this, 'create_post_by_email_address' ) );
+ add_action( 'wp_ajax_jetpack_post_by_email_regenerate', array( &$this, 'regenerate_post_by_email_address' ) );
+ add_action( 'wp_ajax_jetpack_post_by_email_disable', array( &$this, 'delete_post_by_email_address' ) );
+ }
+
+ function profile_scripts() {
+ wp_enqueue_script( 'post-by-email', plugins_url( 'post-by-email/post-by-email.js', __FILE__ ), array( 'jquery' ) );
+ wp_localize_script( 'post-by-email', 'pbeVars', array(
+ 'nonces' => array(
+ 'enable' => wp_create_nonce( 'jetpack.createPostByEmailAddress' ),
+ 'regenerate' => wp_create_nonce( 'jetpack.regeneratePostByEmailAddress' ),
+ 'disable' => wp_create_nonce( 'jetpack.deletePostByEmailAddress' ),
+ ),
+ ));
+ wp_enqueue_style( 'post-by-email', plugins_url( 'post-by-email/post-by-email.css', __FILE__ ) );
+ wp_style_add_data( 'post-by-email', 'jetpack-inline', true );
+ // Do we really need `admin_styles`? With the new admin UI, it's breaking some bits.
+ // Jetpack::init()->admin_styles();
+ }
+
+ function check_user_connection() {
+ $user_token = Jetpack_Data::get_access_token( get_current_user_id() );
+ $is_user_connected = $user_token && !is_wp_error( $user_token );
+
+ // If the user is already connected via Jetpack, then we're good
+ if ( $is_user_connected )
+ return true;
+
+ return false;
+ }
+
+ function user_profile() {
+ $blog_name = get_bloginfo( 'blogname' );
+ if ( empty( $blog_name ) ) {
+ $blog_name = home_url( '/' );
+ }
+
+ ?>
+ <div id="post-by-email" class="jetpack-targetable">
+ <h3><?php esc_html_e( 'Post by Email', 'jetpack' ); ?></h3>
+ <table class="form-table">
+ <tr>
+ <th scope="row"><?php esc_html_e( 'Email Address', 'jetpack' ); ?><span id="jp-pbe-spinner" class="spinner"></span></th>
+ <td>
+ <div id="jp-pbe-error" class="jetpack-inline-error"></div> <?php
+
+ if ( $this->check_user_connection() ) {
+ $email = $this->get_post_by_email_address();
+
+ if ( empty( $email ) ) {
+ $enable_hidden = '';
+ $info_hidden = ' style="display: none;"';
+ } else {
+ $enable_hidden = ' style="display: none;"';
+ $info_hidden = '';
+ } ?>
+
+ <input type="button" name="jp-pbe-enable" id="jp-pbe-enable" class="button" value="<?php esc_attr_e( 'Enable Post By Email', 'jetpack' ); ?> "<?php echo $enable_hidden; ?> />
+ <div id="jp-pbe-info"<?php echo $info_hidden; ?>>
+ <p id="jp-pbe-email-wrapper">
+ <input type="text" id="jp-pbe-email" value="<?php echo esc_attr( $email ); ?>" readonly="readonly" class="regular-text" />
+ <span class="description"><a target="_blank" href="http://jetpack.com/support/post-by-email/"><?php esc_html_e( 'More information', 'jetpack' ); ?></a></span>
+ </p>
+ <p>
+ <input type="button" name="jp-pbe-regenerate" id="jp-pbe-regenerate" class="button" value="<?php esc_attr_e( 'Regenerate Address', 'jetpack' ); ?> " />
+ <input type="button" name="jp-pbe-disable" id="jp-pbe-disable" class="button" value="<?php esc_attr_e( 'Disable Post By Email', 'jetpack' ); ?> " />
+ </p>
+ </div> <?php
+ } else {
+ $jetpack = Jetpack::init(); ?>
+
+ <p class="jetpack-inline-message">
+ <?php printf(
+ esc_html( wptexturize( __( 'To use Post By Email, you need to link your %s account to your WordPress.com account.', 'jetpack' ) ) ),
+ '<strong>' . esc_html( $blog_name ) . '</strong>'
+ ); ?><br />
+ <?php echo esc_html( wptexturize( __( "If you don't have a WordPress.com account yet, you can sign up for free in just a few seconds.", 'jetpack' ) ) ); ?>
+ </p>
+ <p>
+ <a href="<?php echo $jetpack->build_connect_url( false, get_edit_profile_url( get_current_user_id() ) . '#post-by-email', 'unlinked-user-pbe' ); ?>" class="button button-connector" id="wpcom-connect"><?php esc_html_e( 'Link account with WordPress.com', 'jetpack' ); ?></a>
+ </p>
+ <?php
+ } ?>
+ </td>
+ </tr>
+ </table>
+ </div>
+ <?php
+ }
+
+ function get_post_by_email_address() {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id(),
+ ) );
+ $xml->query( 'jetpack.getPostByEmailAddress' );
+
+ if ( $xml->isError() )
+ return NULL;
+
+ $response = $xml->getResponse();
+ if ( empty( $response ) )
+ return NULL;
+
+ return $response;
+ }
+
+ function create_post_by_email_address() {
+ self::__process_ajax_proxy_request(
+ 'jetpack.createPostByEmailAddress',
+ __( 'Unable to create your Post By Email address. Please try again later.', 'jetpack' )
+ );
+ }
+
+ function regenerate_post_by_email_address() {
+ self::__process_ajax_proxy_request(
+ 'jetpack.regeneratePostByEmailAddress',
+ __( 'Unable to regenerate your Post By Email address. Please try again later.', 'jetpack' )
+ );
+ }
+
+ function delete_post_by_email_address() {
+ self::__process_ajax_proxy_request(
+ 'jetpack.deletePostByEmailAddress',
+ __( 'Unable to disable your Post By Email address. Please try again later.', 'jetpack' )
+ );
+ }
+
+ /**
+ * Back end function to abstract the xmlrpc function calls to wpcom.
+ *
+ * @param $endpoint
+ * @param $error_message
+ */
+ function __process_ajax_proxy_request( $endpoint, $error_message ) { // phpcs:ignore
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error( $error_message );
+ }
+ if ( empty( $_REQUEST['pbe_nonce'] ) || ! wp_verify_nonce( $_REQUEST['pbe_nonce'], $endpoint ) ) {
+ wp_send_json_error( $error_message );
+ }
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id(),
+ ) );
+ $xml->query( $endpoint );
+
+ if ( $xml->isError() ) {
+ wp_send_json_error( $error_message );
+ }
+
+ $response = $xml->getResponse();
+ if ( empty( $response ) ) {
+ wp_send_json_error( $error_message );
+ }
+
+ // Will be used only in Jetpack_Core_Json_Api_Endpoints::get_remote_value.
+ update_option( 'post_by_email_address' . get_current_user_id(), $response );
+
+ wp_send_json_success( $response );
+ }
+}
diff --git a/plugins/jetpack/modules/post-by-email/post-by-email-rtl.css b/plugins/jetpack/modules/post-by-email/post-by-email-rtl.css
new file mode 100644
index 00000000..46768086
--- /dev/null
+++ b/plugins/jetpack/modules/post-by-email/post-by-email-rtl.css
@@ -0,0 +1,7 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#jp-pbe-error {
+ display: none;
+}
+#post-by-email:target .jetpack-inline-message {
+ background-color: #fff;
+}
diff --git a/plugins/jetpack/modules/post-by-email/post-by-email-rtl.min.css b/plugins/jetpack/modules/post-by-email/post-by-email-rtl.min.css
new file mode 100644
index 00000000..a4e020bb
--- /dev/null
+++ b/plugins/jetpack/modules/post-by-email/post-by-email-rtl.min.css
@@ -0,0 +1 @@
+#jp-pbe-error{display:none}#post-by-email:target .jetpack-inline-message{background-color:#fff} \ No newline at end of file
diff --git a/plugins/jetpack/modules/post-by-email/post-by-email.css b/plugins/jetpack/modules/post-by-email/post-by-email.css
new file mode 100644
index 00000000..c3b88def
--- /dev/null
+++ b/plugins/jetpack/modules/post-by-email/post-by-email.css
@@ -0,0 +1,6 @@
+#jp-pbe-error {
+ display: none;
+}
+#post-by-email:target .jetpack-inline-message {
+ background-color: #fff;
+}
diff --git a/plugins/jetpack/modules/post-by-email/post-by-email.js b/plugins/jetpack/modules/post-by-email/post-by-email.js
new file mode 100644
index 00000000..a8141684
--- /dev/null
+++ b/plugins/jetpack/modules/post-by-email/post-by-email.js
@@ -0,0 +1,128 @@
+/* global jetpack_post_by_email:true, ajaxurl, pbeVars */
+
+( function( $ ) {
+ var $pbeDisable,
+ $pbeEmail,
+ $pbeEmailWrapper,
+ $pbeEnable,
+ $pbeError,
+ $pbeInfo,
+ $pbeRegenerate,
+ $pbeSpinner;
+
+ jetpack_post_by_email = {
+ init: function() {
+ $pbeEnable.click( jetpack_post_by_email.enable );
+ $pbeRegenerate.click( jetpack_post_by_email.regenerate );
+ $pbeDisable.click( jetpack_post_by_email.disable );
+ },
+
+ enable: function() {
+ $pbeEnable.attr( 'disabled', 'disabled' );
+ $pbeError.fadeOut();
+ $pbeSpinner.fadeIn();
+
+ var data = {
+ action: 'jetpack_post_by_email_enable',
+ pbe_nonce: pbeVars.nonces.enable,
+ };
+
+ $.post( ajaxurl, data, jetpack_post_by_email.handle_enabled );
+ },
+
+ handle_enabled: function( response ) {
+ $pbeRegenerate.removeAttr( 'disabled' );
+ $pbeDisable.removeAttr( 'disabled' );
+
+ if ( response.success ) {
+ $pbeEnable.fadeOut( 400, function() {
+ $pbeEnable.removeAttr( 'disabled' );
+ $pbeEmail.val( response.data );
+ $pbeInfo.fadeIn();
+ } );
+ } else {
+ $pbeError.text( response.data );
+ $pbeError.fadeIn();
+ $pbeEnable.removeAttr( 'disabled' );
+ }
+
+ $pbeSpinner.fadeOut();
+ },
+
+ regenerate: function() {
+ $pbeRegenerate.attr( 'disabled', 'disabled' );
+ $pbeDisable.attr( 'disabled', 'disabled' );
+ $pbeError.fadeOut();
+ $pbeSpinner.fadeIn();
+
+ var data = {
+ action: 'jetpack_post_by_email_regenerate',
+ pbe_nonce: pbeVars.nonces.regenerate,
+ };
+
+ $.post( ajaxurl, data, jetpack_post_by_email.handle_regenerated );
+ },
+
+ handle_regenerated: function( response ) {
+ if ( response.success ) {
+ $pbeEmailWrapper.fadeOut( 400, function() {
+ $pbeEmail.val( response.data );
+ $pbeEmailWrapper.fadeIn();
+ } );
+ } else {
+ $pbeError.text( response.data );
+ $pbeError.fadeIn();
+ }
+
+ $pbeRegenerate.removeAttr( 'disabled' );
+ $pbeDisable.removeAttr( 'disabled' );
+ $pbeSpinner.fadeOut();
+ },
+
+ disable: function() {
+ $pbeRegenerate.attr( 'disabled', 'disabled' );
+ $pbeDisable.attr( 'disabled', 'disabled' );
+ $pbeError.fadeOut();
+ $pbeSpinner.fadeIn();
+
+ var data = {
+ action: 'jetpack_post_by_email_disable',
+ pbe_nonce: pbeVars.nonces.disable,
+ };
+
+ $.post( ajaxurl, data, jetpack_post_by_email.handle_disabled );
+ },
+
+ handle_disabled: function( response ) {
+ if ( response.success ) {
+ $pbeEnable.removeAttr( 'disabled' );
+ $pbeInfo.fadeOut( 400, function() {
+ $pbeRegenerate.removeAttr( 'disabled' );
+ $pbeDisable.removeAttr( 'disabled' );
+ $pbeEnable.fadeIn();
+ } );
+ } else {
+ $pbeRegenerate.removeAttr( 'disabled' );
+ $pbeDisable.removeAttr( 'disabled' );
+
+ $pbeError.text( response.data );
+ $pbeError.fadeIn();
+ }
+
+ $pbeSpinner.fadeOut();
+ },
+ };
+
+ $( function() {
+ $pbeDisable = $( '#jp-pbe-disable' );
+ $pbeEmail = $( '#jp-pbe-email' );
+ $pbeEmailWrapper = $( '#jp-pbe-email-wrapper' );
+ $pbeEnable = $( '#jp-pbe-enable' );
+ $pbeError = $( '#jp-pbe-error' );
+ $pbeInfo = $( '#jp-pbe-info' );
+ $pbeRegenerate = $( '#jp-pbe-regenerate' );
+ $pbeSpinner = $( '#jp-pbe-spinner' );
+
+ jetpack_post_by_email.init();
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/post-by-email/post-by-email.min.css b/plugins/jetpack/modules/post-by-email/post-by-email.min.css
new file mode 100644
index 00000000..bfb7b63f
--- /dev/null
+++ b/plugins/jetpack/modules/post-by-email/post-by-email.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#jp-pbe-error{display:none}#post-by-email:target .jetpack-inline-message{background-color:#fff} \ No newline at end of file
diff --git a/plugins/jetpack/modules/protect.php b/plugins/jetpack/modules/protect.php
new file mode 100644
index 00000000..334439fc
--- /dev/null
+++ b/plugins/jetpack/modules/protect.php
@@ -0,0 +1,882 @@
+<?php
+/**
+ * Module Name: Protect
+ * Module Description: Protect yourself from brute force and distributed brute force attacks, which are the most common way for hackers to get into your site.
+ * Sort Order: 1
+ * Recommendation Order: 4
+ * First Introduced: 3.4
+ * Requires Connection: Yes
+ * Auto Activate: Yes
+ * Module Tags: Recommended
+ * Feature: Security
+ * Additional Search Queries: security, jetpack protect, secure, protection, botnet, brute force, protect, login, bot, password, passwords, strong passwords, strong password, wp-login.php, protect admin
+ */
+
+include_once JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php';
+
+class Jetpack_Protect_Module {
+
+ private static $__instance = null;
+ public $api_key;
+ public $api_key_error;
+ public $whitelist;
+ public $whitelist_error;
+ public $whitelist_saved;
+ private $user_ip;
+ private $local_host;
+ private $api_endpoint;
+ public $last_request;
+ public $last_response_raw;
+ public $last_response;
+ private $block_login_with_math;
+
+ /**
+ * Singleton implementation
+ *
+ * @return object
+ */
+ public static function instance() {
+ if ( ! is_a( self::$__instance, 'Jetpack_Protect_Module' ) ) {
+ self::$__instance = new Jetpack_Protect_Module();
+ }
+
+ return self::$__instance;
+ }
+
+ /**
+ * Registers actions
+ */
+ private function __construct() {
+ add_action( 'jetpack_activate_module_protect', array ( $this, 'on_activation' ) );
+ add_action( 'jetpack_deactivate_module_protect', array ( $this, 'on_deactivation' ) );
+ add_action( 'jetpack_modules_loaded', array ( $this, 'modules_loaded' ) );
+ add_action( 'login_form', array ( $this, 'check_use_math' ), 0 );
+ add_filter( 'authenticate', array ( $this, 'check_preauth' ), 10, 3 );
+ add_action( 'wp_login', array ( $this, 'log_successful_login' ), 10, 2 );
+ add_action( 'wp_login_failed', array ( $this, 'log_failed_attempt' ) );
+ add_action( 'admin_init', array ( $this, 'maybe_update_headers' ) );
+ add_action( 'admin_init', array ( $this, 'maybe_display_security_warning' ) );
+
+ // This is a backup in case $pagenow fails for some reason
+ add_action( 'login_form', array ( $this, 'check_login_ability' ), 1 );
+
+ // Load math fallback after math page form submission
+ if ( isset( $_POST[ 'jetpack_protect_process_math_form' ] ) ) {
+ include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
+ new Jetpack_Protect_Math_Authenticate;
+ }
+
+ // Runs a script every day to clean up expired transients so they don't
+ // clog up our users' databases
+ require_once( JETPACK__PLUGIN_DIR . '/modules/protect/transient-cleanup.php' );
+ }
+
+ /**
+ * On module activation, try to get an api key
+ */
+ public function on_activation() {
+ if ( is_multisite() && is_main_site() && get_site_option( 'jetpack_protect_active', 0 ) == 0 ) {
+ update_site_option( 'jetpack_protect_active', 1 );
+ }
+
+ update_site_option( 'jetpack_protect_activating', 'activating' );
+
+ // Get BruteProtect's counter number
+ Jetpack_Protect_Module::protect_call( 'check_key' );
+ }
+
+ /**
+ * On module deactivation, unset protect_active
+ */
+ public function on_deactivation() {
+ if ( is_multisite() && is_main_site() ) {
+ update_site_option( 'jetpack_protect_active', 0 );
+ }
+ }
+
+ public function maybe_get_protect_key() {
+ if ( get_site_option( 'jetpack_protect_activating', false ) && ! get_site_option( 'jetpack_protect_key', false ) ) {
+ $key = $this->get_protect_key();
+ delete_site_option( 'jetpack_protect_activating' );
+ return $key;
+ }
+
+ return get_site_option( 'jetpack_protect_key' );
+ }
+
+ /**
+ * Sends a "check_key" API call once a day. This call allows us to track IP-related
+ * headers for this server via the Protect API, in order to better identify the source
+ * IP for login attempts
+ */
+ public function maybe_update_headers( $force = false ) {
+ $updated_recently = $this->get_transient( 'jpp_headers_updated_recently' );
+
+ if ( ! $force ) {
+ if ( isset( $_GET['protect_update_headers'] ) ) {
+ $force = true;
+ }
+ }
+
+ // check that current user is admin so we prevent a lower level user from adding
+ // a trusted header, allowing them to brute force an admin account
+ if ( ( $updated_recently && ! $force ) || ! current_user_can( 'update_plugins' ) ) {
+ return;
+ }
+
+ $response = Jetpack_Protect_Module::protect_call( 'check_key' );
+ $this->set_transient( 'jpp_headers_updated_recently', 1, DAY_IN_SECONDS );
+
+ if ( isset( $response['msg'] ) && $response['msg'] ) {
+ update_site_option( 'trusted_ip_header', json_decode( $response['msg'] ) );
+ }
+
+ }
+
+ public function maybe_display_security_warning() {
+ if ( is_multisite() && current_user_can( 'manage_network' ) ) {
+ if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
+ require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
+ }
+
+ if ( ! ( is_plugin_active_for_network( 'jetpack/jetpack.php' ) || is_plugin_active_for_network( 'jetpack-dev/jetpack.php' ) ) ) {
+ add_action( 'load-index.php', array ( $this, 'prepare_jetpack_protect_multisite_notice' ) );
+ }
+ }
+ }
+
+ public function prepare_jetpack_protect_multisite_notice() {
+ add_action( 'admin_print_styles', array ( $this, 'admin_banner_styles' ) );
+ add_action( 'admin_notices', array ( $this, 'admin_jetpack_manage_notice' ) );
+ }
+
+ public function admin_banner_styles() {
+ global $wp_styles;
+
+ $min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
+
+ wp_enqueue_style( 'jetpack', plugins_url( "css/jetpack-banners{$min}.css", JETPACK__PLUGIN_FILE ), false, JETPACK__VERSION );
+ $wp_styles->add_data( 'jetpack', 'rtl', true );
+ }
+
+ public function admin_jetpack_manage_notice() {
+
+ $dismissed = get_site_option( 'jetpack_dismissed_protect_multisite_banner' );
+
+ if ( $dismissed ) {
+ return;
+ }
+
+ $referer = '&_wp_http_referer=' . add_query_arg( '_wp_http_referer', null );
+ $opt_out_url = wp_nonce_url( Jetpack::admin_url( 'jetpack-notice=jetpack-protect-multisite-opt-out' . $referer ), 'jetpack_protect_multisite_banner_opt_out' );
+
+ ?>
+ <div id="message" class="updated jetpack-message jp-banner is-opt-in protect-error"
+ style="display:block !important;">
+ <a class="jp-banner__dismiss" href="<?php echo esc_url( $opt_out_url ); ?>"
+ title="<?php esc_attr_e( 'Dismiss this notice.', 'jetpack' ); ?>"></a>
+
+ <div class="jp-banner__content">
+ <h2><?php esc_html_e( 'Protect cannot keep your site secure.', 'jetpack' ); ?></h2>
+
+ <p><?php printf( __( 'Thanks for activating Protect! To start protecting your site, please network activate Jetpack on your Multisite installation and activate Protect on your primary site. Due to the way logins are handled on WordPress Multisite, Jetpack must be network-enabled in order for Protect to work properly. <a href="%s" target="_blank">Learn More</a>', 'jetpack' ), 'http://jetpack.com/support/multisite-protect' ); ?></p>
+ </div>
+ <div class="jp-banner__action-container is-opt-in">
+ <a href="<?php echo esc_url( network_admin_url( 'plugins.php' ) ); ?>" class="jp-banner__button"
+ id="wpcom-connect"><?php _e( 'View Network Admin', 'jetpack' ); ?></a>
+ </div>
+ </div>
+ <?php
+ }
+
+ /**
+ * Request an api key from wordpress.com
+ *
+ * @return bool | string
+ */
+ public function get_protect_key() {
+
+ $protect_blog_id = Jetpack_Protect_Module::get_main_blog_jetpack_id();
+
+ // If we can't find the the blog id, that means we are on multisite, and the main site never connected
+ // the protect api key is linked to the main blog id - instruct the user to connect their main blog
+ if ( ! $protect_blog_id ) {
+ $this->api_key_error = __( 'Your main blog is not connected to WordPress.com. Please connect to get an API key.', 'jetpack' );
+
+ return false;
+ }
+
+ $request = array (
+ 'jetpack_blog_id' => $protect_blog_id,
+ 'bruteprotect_api_key' => get_site_option( 'bruteprotect_api_key' ),
+ 'multisite' => '0',
+ );
+
+ // Send the number of blogs on the network if we are on multisite
+ if ( is_multisite() ) {
+ $request['multisite'] = get_blog_count();
+ if ( ! $request['multisite'] ) {
+ global $wpdb;
+ $request['multisite'] = $wpdb->get_var( "SELECT COUNT(blog_id) as c FROM $wpdb->blogs WHERE spam = '0' AND deleted = '0' and archived = '0'" );
+ }
+ }
+
+ // Request the key
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array (
+ 'user_id' => get_current_user_id()
+ ) );
+ $xml->query( 'jetpack.protect.requestKey', $request );
+
+ // Hmm, can't talk to wordpress.com
+ if ( $xml->isError() ) {
+ $code = $xml->getErrorCode();
+ $message = $xml->getErrorMessage();
+ $this->api_key_error = sprintf( __( 'Error connecting to WordPress.com. Code: %1$s, %2$s', 'jetpack' ), $code, $message );
+
+ return false;
+ }
+
+ $response = $xml->getResponse();
+
+ // Hmm. Can't talk to the protect servers ( api.bruteprotect.com )
+ if ( ! isset( $response['data'] ) ) {
+ $this->api_key_error = __( 'No reply from Jetpack servers', 'jetpack' );
+
+ return false;
+ }
+
+ // There was an issue generating the key
+ if ( empty( $response['success'] ) ) {
+ $this->api_key_error = $response['data'];
+
+ return false;
+ }
+
+ // Key generation successful!
+ $active_plugins = Jetpack::get_active_plugins();
+
+ // We only want to deactivate BruteProtect if we successfully get a key
+ if ( in_array( 'bruteprotect/bruteprotect.php', $active_plugins ) ) {
+ Jetpack_Client_Server::deactivate_plugin( 'bruteprotect/bruteprotect.php', 'BruteProtect' );
+ }
+
+ $key = $response['data'];
+ update_site_option( 'jetpack_protect_key', $key );
+
+ return $key;
+ }
+
+ /**
+ * Called via WP action wp_login_failed to log failed attempt with the api
+ *
+ * Fires custom, plugable action jpp_log_failed_attempt with the IP
+ *
+ * @return void
+ */
+ function log_failed_attempt( $login_user = null ) {
+
+ /**
+ * Fires before every failed login attempt.
+ *
+ * @module protect
+ *
+ * @since 3.4.0
+ *
+ * @param array Information about failed login attempt
+ * [
+ * 'login' => (string) Username or email used in failed login attempt
+ * ]
+ */
+ do_action( 'jpp_log_failed_attempt', array( 'login' => $login_user ) );
+
+ if ( isset( $_COOKIE['jpp_math_pass'] ) ) {
+
+ $transient = $this->get_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'] );
+ $transient--;
+
+ if ( ! $transient || $transient < 1 ) {
+ $this->delete_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'] );
+ setcookie( 'jpp_math_pass', 0, time() - DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false );
+ } else {
+ $this->set_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'], $transient, DAY_IN_SECONDS );
+ }
+
+ }
+ $this->protect_call( 'failed_attempt' );
+ }
+
+ /**
+ * Set up the Protect configuration page
+ */
+ public function modules_loaded() {
+ Jetpack::enable_module_configurable( __FILE__ );
+ }
+
+ /**
+ * Logs a successful login back to our servers, this allows us to make sure we're not blocking
+ * a busy IP that has a lot of good logins along with some forgotten passwords. Also saves current user's ip
+ * to the ip address whitelist
+ */
+ public function log_successful_login( $user_login, $user = null ) {
+ if ( ! $user ) { // For do_action( 'wp_login' ) calls that lacked passing the 2nd arg.
+ $user = get_user_by( 'login', $user_login );
+ }
+
+ $this->protect_call( 'successful_login', array ( 'roles' => $user->roles ) );
+ }
+
+
+ /**
+ * Checks for loginability BEFORE authentication so that bots don't get to go around the log in form.
+ *
+ * If we are using our math fallback, authenticate via math-fallback.php
+ *
+ * @param string $user
+ * @param string $username
+ * @param string $password
+ *
+ * @return string $user
+ */
+ function check_preauth( $user = 'Not Used By Protect', $username = 'Not Used By Protect', $password = 'Not Used By Protect' ) {
+ $allow_login = $this->check_login_ability( true );
+ $use_math = $this->get_transient( 'brute_use_math' );
+
+ if ( ! $allow_login ) {
+ $this->block_with_math();
+ }
+
+ if ( ( 1 == $use_math || 1 == $this->block_login_with_math ) && isset( $_POST['log'] ) ) {
+ include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
+ Jetpack_Protect_Math_Authenticate::math_authenticate();
+ }
+
+ return $user;
+ }
+
+ /**
+ * Get all IP headers so that we can process on our server...
+ *
+ * @return string
+ */
+ function get_headers() {
+ $ip_related_headers = array (
+ 'GD_PHP_HANDLER',
+ 'HTTP_AKAMAI_ORIGIN_HOP',
+ 'HTTP_CF_CONNECTING_IP',
+ 'HTTP_CLIENT_IP',
+ 'HTTP_FASTLY_CLIENT_IP',
+ 'HTTP_FORWARDED',
+ 'HTTP_FORWARDED_FOR',
+ 'HTTP_INCAP_CLIENT_IP',
+ 'HTTP_TRUE_CLIENT_IP',
+ 'HTTP_X_CLIENTIP',
+ 'HTTP_X_CLUSTER_CLIENT_IP',
+ 'HTTP_X_FORWARDED',
+ 'HTTP_X_FORWARDED_FOR',
+ 'HTTP_X_IP_TRAIL',
+ 'HTTP_X_REAL_IP',
+ 'HTTP_X_VARNISH',
+ 'REMOTE_ADDR'
+ );
+
+ foreach ( $ip_related_headers as $header ) {
+ if ( isset( $_SERVER[ $header ] ) ) {
+ $output[ $header ] = $_SERVER[ $header ];
+ }
+ }
+
+ return $output;
+ }
+
+ /*
+ * Checks if the IP address has been whitelisted
+ *
+ * @param string $ip
+ *
+ * @return bool
+ */
+ function ip_is_whitelisted( $ip ) {
+ // If we found an exact match in wp-config
+ if ( defined( 'JETPACK_IP_ADDRESS_OK' ) && JETPACK_IP_ADDRESS_OK == $ip ) {
+ return true;
+ }
+
+ $whitelist = jetpack_protect_get_local_whitelist();
+
+ if ( is_multisite() ) {
+ $whitelist = array_merge( $whitelist, get_site_option( 'jetpack_protect_global_whitelist', array () ) );
+ }
+
+ if ( ! empty( $whitelist ) ) :
+ foreach ( $whitelist as $item ) :
+ // If the IPs are an exact match
+ if ( ! $item->range && isset( $item->ip_address ) && $item->ip_address == $ip ) {
+ return true;
+ }
+
+ if ( $item->range && isset( $item->range_low ) && isset( $item->range_high ) ) {
+ if ( jetpack_protect_ip_address_is_in_range( $ip, $item->range_low, $item->range_high ) ) {
+ return true;
+ }
+ }
+ endforeach;
+ endif;
+
+ return false;
+ }
+
+ /**
+ * Checks the status for a given IP. API results are cached as transients
+ *
+ * @param bool $preauth Whether or not we are checking prior to authorization
+ *
+ * @return bool Either returns true, fires $this->kill_login, or includes a math fallback and returns false
+ */
+ function check_login_ability( $preauth = false ) {
+
+ /**
+ * JETPACK_ALWAYS_PROTECT_LOGIN will always disable the login page, and use a page provided by Jetpack.
+ */
+ if ( Jetpack_Constants::is_true( 'JETPACK_ALWAYS_PROTECT_LOGIN' ) ) {
+ $this->kill_login();
+ }
+
+ if ( $this->is_current_ip_whitelisted() ) {
+ return true;
+ }
+
+ $status = $this->get_cached_status();
+
+ if ( empty( $status ) ) {
+ // If we've reached this point, this means that the IP isn't cached.
+ // Now we check with the Protect API to see if we should allow login
+ $response = $this->protect_call( $action = 'check_ip' );
+
+ if ( isset( $response['math'] ) && ! function_exists( 'brute_math_authenticate' ) ) {
+ include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
+ new Jetpack_Protect_Math_Authenticate;
+
+ return false;
+ }
+
+ $status = $response['status'];
+ }
+
+ if ( 'blocked' == $status ) {
+ $this->block_with_math();
+ }
+
+ if ( 'blocked-hard' == $status ) {
+ $this->kill_login();
+ }
+
+ return true;
+ }
+
+ function is_current_ip_whitelisted() {
+ $ip = jetpack_protect_get_ip();
+
+ // Server is misconfigured and we can't get an IP
+ if ( ! $ip && class_exists( 'Jetpack' ) ) {
+ Jetpack::deactivate_module( 'protect' );
+ ob_start();
+ Jetpack::state( 'message', 'protect_misconfigured_ip' );
+ ob_end_clean();
+ return true;
+ }
+
+ /**
+ * Short-circuit check_login_ability.
+ *
+ * If there is an alternate way to validate the current IP such as
+ * a hard-coded list of IP addresses, we can short-circuit the rest
+ * of the login ability checks and return true here.
+ *
+ * @module protect
+ *
+ * @since 4.4.0
+ *
+ * @param bool false Should we allow all logins for the current ip? Default: false
+ */
+ if ( apply_filters( 'jpp_allow_login', false, $ip ) ) {
+ return true;
+ }
+
+ if ( jetpack_protect_ip_is_private( $ip ) ) {
+ return true;
+ }
+
+ if ( $this->ip_is_whitelisted( $ip ) ) {
+ return true;
+ }
+ }
+
+ function has_login_ability() {
+ if ( $this->is_current_ip_whitelisted() ) {
+ return true;
+ }
+ $status = $this->get_cached_status();
+ if ( empty( $status ) || $status === 'ok' ) {
+ return true;
+ }
+ return false;
+ }
+
+ function get_cached_status() {
+ $transient_name = $this->get_transient_name();
+ $value = $this->get_transient( $transient_name );
+ if ( isset( $value['status'] ) ) {
+ return $value['status'];
+ }
+ return '';
+ }
+
+ function block_with_math() {
+ /**
+ * By default, Protect will allow a user who has been blocked for too
+ * many failed logins to start answering math questions to continue logging in
+ *
+ * For added security, you can disable this.
+ *
+ * @module protect
+ *
+ * @since 3.6.0
+ *
+ * @param bool Whether to allow math for blocked users or not.
+ */
+
+ $this->block_login_with_math = 1;
+ /**
+ * Allow Math fallback for blocked IPs.
+ *
+ * @module protect
+ *
+ * @since 3.6.0
+ *
+ * @param bool true Should we fallback to the Math questions when an IP is blocked. Default to true.
+ */
+ $allow_math_fallback_on_fail = apply_filters( 'jpp_use_captcha_when_blocked', true );
+ if ( ! $allow_math_fallback_on_fail ) {
+ $this->kill_login();
+ }
+ include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
+ new Jetpack_Protect_Math_Authenticate;
+
+ return false;
+ }
+
+ /*
+ * Kill a login attempt
+ */
+ function kill_login() {
+ if (
+ isset( $_GET['action'], $_GET['_wpnonce'] ) &&
+ 'logout' === $_GET['action'] &&
+ wp_verify_nonce( $_GET['_wpnonce'], 'log-out' ) &&
+ wp_get_current_user()
+
+ ) {
+ // Allow users to logout
+ return;
+ }
+
+ $ip = jetpack_protect_get_ip();
+ /**
+ * Fires before every killed login.
+ *
+ * @module protect
+ *
+ * @since 3.4.0
+ *
+ * @param string $ip IP flagged by Protect.
+ */
+ do_action( 'jpp_kill_login', $ip );
+
+ if( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
+ $die_string = sprintf( __( 'Your IP (%1$s) has been flagged for potential security violations.', 'jetpack' ), str_replace( 'http://', '', esc_url( 'http://' . $ip ) ) );
+ wp_die(
+ $die_string,
+ __( 'Login Blocked by Jetpack', 'jetpack' ),
+ array ( 'response' => 403 )
+ );
+ }
+
+ require_once dirname( __FILE__ ) . '/protect/blocked-login-page.php';
+ $blocked_login_page = Jetpack_Protect_Blocked_Login_Page::instance( $ip );
+
+ if ( $blocked_login_page->is_blocked_user_valid() ) {
+ return;
+ }
+
+ $blocked_login_page->render_and_die();
+ }
+
+ /*
+ * Checks if the protect API call has failed, and if so initiates the math captcha fallback.
+ */
+ public function check_use_math() {
+ $use_math = $this->get_transient( 'brute_use_math' );
+ if ( $use_math ) {
+ include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
+ new Jetpack_Protect_Math_Authenticate;
+ }
+ }
+
+ /**
+ * If we're in a multisite network, return the blog ID of the primary blog
+ *
+ * @return int
+ */
+ public function get_main_blog_id() {
+ if ( ! is_multisite() ) {
+ return false;
+ }
+
+ global $current_site;
+ $primary_blog_id = $current_site->blog_id;
+
+ return $primary_blog_id;
+ }
+
+ /**
+ * Get jetpack blog id, or the jetpack blog id of the main blog in the main network
+ *
+ * @return int
+ */
+ public function get_main_blog_jetpack_id() {
+ if ( ! is_main_site() ) {
+ switch_to_blog( $this->get_main_blog_id() );
+ $id = Jetpack::get_option( 'id', false );
+ restore_current_blog();
+ } else {
+ $id = Jetpack::get_option( 'id' );
+ }
+
+ return $id;
+ }
+
+ public function check_api_key() {
+ $response = $this->protect_call( 'check_key' );
+
+ if ( isset( $response['ckval'] ) ) {
+ return true;
+ }
+
+ if ( isset( $response['error'] ) ) {
+
+ if ( $response['error'] == 'Invalid API Key' ) {
+ $this->api_key_error = __( 'Your API key is invalid', 'jetpack' );
+ }
+
+ if ( $response['error'] == 'API Key Required' ) {
+ $this->api_key_error = __( 'No API key', 'jetpack' );
+ }
+ }
+
+ $this->api_key_error = __( 'There was an error contacting Jetpack servers.', 'jetpack' );
+
+ return false;
+ }
+
+ /**
+ * Calls over to the api using wp_remote_post
+ *
+ * @param string $action 'check_ip', 'check_key', or 'failed_attempt'
+ * @param array $request Any custom data to post to the api
+ *
+ * @return array
+ */
+ function protect_call( $action = 'check_ip', $request = array () ) {
+ global $wp_version;
+
+ $api_key = $this->maybe_get_protect_key();
+
+ $user_agent = "WordPress/{$wp_version} | Jetpack/" . constant( 'JETPACK__VERSION' );
+
+ $request['action'] = $action;
+ $request['ip'] = jetpack_protect_get_ip();
+ $request['host'] = $this->get_local_host();
+ $request['headers'] = json_encode( $this->get_headers() );
+ $request['jetpack_version'] = constant( 'JETPACK__VERSION' );
+ $request['wordpress_version'] = strval( $wp_version );
+ $request['api_key'] = $api_key;
+ $request['multisite'] = "0";
+
+ if ( is_multisite() ) {
+ $request['multisite'] = get_blog_count();
+ }
+
+
+ /**
+ * Filter controls maximum timeout in waiting for reponse from Protect servers.
+ *
+ * @module protect
+ *
+ * @since 4.0.4
+ *
+ * @param int $timeout Max time (in seconds) to wait for a response.
+ */
+ $timeout = apply_filters( 'jetpack_protect_connect_timeout', 30 );
+
+ $args = array (
+ 'body' => $request,
+ 'user-agent' => $user_agent,
+ 'httpversion' => '1.0',
+ 'timeout' => absint( $timeout )
+ );
+
+ $response_json = wp_remote_post( $this->get_api_host(), $args );
+ $this->last_response_raw = $response_json;
+
+ $transient_name = $this->get_transient_name();
+ $this->delete_transient( $transient_name );
+
+ if ( is_array( $response_json ) ) {
+ $response = json_decode( $response_json['body'], true );
+ }
+
+ if ( isset( $response['blocked_attempts'] ) && $response['blocked_attempts'] ) {
+ update_site_option( 'jetpack_protect_blocked_attempts', $response['blocked_attempts'] );
+ }
+
+ if ( isset( $response['status'] ) && ! isset( $response['error'] ) ) {
+ $response['expire'] = time() + $response['seconds_remaining'];
+ $this->set_transient( $transient_name, $response, $response['seconds_remaining'] );
+ $this->delete_transient( 'brute_use_math' );
+ } else { // Fallback to Math Captcha if no response from API host
+ $this->set_transient( 'brute_use_math', 1, 600 );
+ $response['status'] = 'ok';
+ $response['math'] = true;
+ }
+
+ if ( isset( $response['error'] ) ) {
+ update_site_option( 'jetpack_protect_error', $response['error'] );
+ } else {
+ delete_site_option( 'jetpack_protect_error' );
+ }
+
+ return $response;
+ }
+
+ function get_transient_name() {
+ $headers = $this->get_headers();
+ $header_hash = md5( json_encode( $headers ) );
+
+ return 'jpp_li_' . $header_hash;
+ }
+
+ /**
+ * Wrapper for WordPress set_transient function, our version sets
+ * the transient on the main site in the network if this is a multisite network
+ *
+ * We do it this way (instead of set_site_transient) because of an issue where
+ * sitewide transients are always autoloaded
+ * https://core.trac.wordpress.org/ticket/22846
+ *
+ * @param string $transient Transient name. Expected to not be SQL-escaped. Must be
+ * 45 characters or fewer in length.
+ * @param mixed $value Transient value. Must be serializable if non-scalar.
+ * Expected to not be SQL-escaped.
+ * @param int $expiration Optional. Time until expiration in seconds. Default 0.
+ *
+ * @return bool False if value was not set and true if value was set.
+ */
+ function set_transient( $transient, $value, $expiration ) {
+ if ( is_multisite() && ! is_main_site() ) {
+ switch_to_blog( $this->get_main_blog_id() );
+ $return = set_transient( $transient, $value, $expiration );
+ restore_current_blog();
+
+ return $return;
+ }
+
+ return set_transient( $transient, $value, $expiration );
+ }
+
+ /**
+ * Wrapper for WordPress delete_transient function, our version deletes
+ * the transient on the main site in the network if this is a multisite network
+ *
+ * @param string $transient Transient name. Expected to not be SQL-escaped.
+ *
+ * @return bool true if successful, false otherwise
+ */
+ function delete_transient( $transient ) {
+ if ( is_multisite() && ! is_main_site() ) {
+ switch_to_blog( $this->get_main_blog_id() );
+ $return = delete_transient( $transient );
+ restore_current_blog();
+
+ return $return;
+ }
+
+ return delete_transient( $transient );
+ }
+
+ /**
+ * Wrapper for WordPress get_transient function, our version gets
+ * the transient on the main site in the network if this is a multisite network
+ *
+ * @param string $transient Transient name. Expected to not be SQL-escaped.
+ *
+ * @return mixed Value of transient.
+ */
+ function get_transient( $transient ) {
+ if ( is_multisite() && ! is_main_site() ) {
+ switch_to_blog( $this->get_main_blog_id() );
+ $return = get_transient( $transient );
+ restore_current_blog();
+
+ return $return;
+ }
+
+ return get_transient( $transient );
+ }
+
+ function get_api_host() {
+ if ( isset( $this->api_endpoint ) ) {
+ return $this->api_endpoint;
+ }
+
+ //Check to see if we can use SSL
+ $this->api_endpoint = Jetpack::fix_url_for_bad_hosts( JETPACK_PROTECT__API_HOST );
+
+ return $this->api_endpoint;
+ }
+
+ function get_local_host() {
+ if ( isset( $this->local_host ) ) {
+ return $this->local_host;
+ }
+
+ $uri = 'http://' . strtolower( $_SERVER['HTTP_HOST'] );
+
+ if ( is_multisite() ) {
+ $uri = network_home_url();
+ }
+
+ $uridata = parse_url( $uri );
+
+ $domain = $uridata['host'];
+
+ // If we still don't have the site_url, get it
+ if ( ! $domain ) {
+ $uri = get_site_url( 1 );
+ $uridata = parse_url( $uri );
+ $domain = $uridata['host'];
+ }
+
+ $this->local_host = $domain;
+
+ return $this->local_host;
+ }
+
+}
+
+$jetpack_protect = Jetpack_Protect_Module::instance();
+
+global $pagenow;
+if ( isset( $pagenow ) && 'wp-login.php' == $pagenow ) {
+ $jetpack_protect->check_login_ability();
+}
diff --git a/plugins/jetpack/modules/protect/blocked-login-page.php b/plugins/jetpack/modules/protect/blocked-login-page.php
new file mode 100644
index 00000000..246031f6
--- /dev/null
+++ b/plugins/jetpack/modules/protect/blocked-login-page.php
@@ -0,0 +1,611 @@
+<?php
+
+
+/**
+ * Class Jetpack_Protect_Blocked_Login_Page
+ *
+ * Instanciated on the wp-login page when Jetpack modules are loaded and $pagenow
+ * is available, or during the login_head hook.
+ *
+ * Class will only be instanciated if Protect has detected a hard blocked IP address.
+ *
+ *
+ */
+class Jetpack_Protect_Blocked_Login_Page {
+
+ private static $__instance = null;
+ public $can_send_recovery_emails;
+ public $ip_address;
+ public $valid_blocked_user_id;
+ public $email_address;
+ const HELP_URL = 'https://jetpack.com/support/security-features/#unblock';
+ const HTTP_STATUS_CODE_TOO_MANY_REQUESTS = 429;
+
+ /**
+ * Singleton implementation
+ *
+ * @return object
+ */
+ public static function instance( $ip_address ) {
+ if ( ! is_a( self::$__instance, 'Jetpack_Protect_Blocked_Login_Page' ) ) {
+ self::$__instance = new Jetpack_Protect_Blocked_Login_Page( $ip_address );
+ }
+
+ return self::$__instance;
+ }
+
+
+ function __construct( $ip_address ) {
+ /**
+ * Filter controls if an email recovery form is shown to blocked IPs.
+ *
+ * A recovery form allows folks to re-gain access to the login form
+ * via an email link if their IP was mistakenly blocked.
+ *
+ * @module protect
+ *
+ * @since 5.6.0
+ *
+ * @param bool $can_send_recovery_emails Defaults to true.
+ */
+ $this->can_send_recovery_emails = apply_filters( 'jetpack_protect_can_send_recovery_emails', true );
+ $this->ip_address = $ip_address;
+
+ add_filter( 'wp_authenticate_user', array( $this, 'check_valid_blocked_user' ), 10, 1 );
+ add_filter( 'site_url', array( $this, 'add_args_to_login_post_url' ), 10, 3 );
+ add_filter( 'network_site_url', array( $this, 'add_args_to_login_post_url' ), 10, 3 );
+ add_filter( 'lostpassword_url', array( $this, 'add_args_to_lostpassword_url' ), 10, 2 );
+ add_filter( 'login_url', array( $this, 'add_args_to_login_url' ), 10, 3 );
+ add_filter( 'lostpassword_redirect', array( $this, 'add_args_to_lostpassword_redirect_url' ), 10, 1 );
+ }
+
+ public function add_args_to_lostpassword_redirect_url( $url ) {
+ if ( $this->valid_blocked_user_id ) {
+ $url = empty( $url ) ? wp_login_url() : $url;
+ $url = add_query_arg(
+ array(
+ 'validate_jetpack_protect_recovery' => $_GET['validate_jetpack_protect_recovery'],
+ 'user_id' => $_GET['user_id'],
+ 'checkemail' => 'confirm',
+ ),
+ $url
+ );
+ }
+
+ return $url;
+ }
+
+ public function add_args_to_lostpassword_url( $url, $redirect ) {
+ if ( $this->valid_blocked_user_id ) {
+ $args = array(
+ 'validate_jetpack_protect_recovery' => $_GET['validate_jetpack_protect_recovery'],
+ 'user_id' => $_GET['user_id'],
+ 'action' => 'lostpassword',
+ );
+ if ( ! empty( $redirect ) ) {
+ $args['redirect_to'] = $redirect;
+ }
+ $url = add_query_arg( $args, $url );
+ }
+
+ return $url;
+ }
+
+ public function add_args_to_login_post_url( $url, $path, $scheme ) {
+ if ( $this->valid_blocked_user_id && ( 'login_post' === $scheme || 'login' === $scheme ) ) {
+ $url = add_query_arg(
+ array(
+ 'validate_jetpack_protect_recovery' => $_GET['validate_jetpack_protect_recovery'],
+ 'user_id' => $_GET['user_id'],
+ ),
+ $url
+ );
+
+ }
+
+ return $url;
+ }
+
+ public function add_args_to_login_url( $url, $redirect, $force_reauth ) {
+ if ( $this->valid_blocked_user_id ) {
+ $args = array(
+ 'validate_jetpack_protect_recovery' => $_GET['validate_jetpack_protect_recovery'],
+ 'user_id' => $_GET['user_id'],
+ );
+
+ if ( ! empty( $redirect ) ) {
+ $args['redirect_to'] = $redirect;
+ }
+
+ if ( ! empty( $force_reauth ) ) {
+ $args['reauth'] = '1';
+ }
+ $url = add_query_arg( $args, $url );
+ }
+
+ return $url;
+ }
+
+ public function check_valid_blocked_user( $user ) {
+ if ( $this->valid_blocked_user_id && $this->valid_blocked_user_id != $user->ID ) {
+ return new WP_Error( 'invalid_recovery_token', __( 'The recovery token is not valid for this user.', 'jetpack' ) );
+ }
+
+ return $user;
+ }
+
+ public function is_blocked_user_valid() {
+ if ( ! $this->can_send_recovery_emails ) {
+ return false;
+ }
+
+ if ( $this->valid_blocked_user_id ) {
+ return true;
+ }
+
+ if ( ! isset( $_GET['validate_jetpack_protect_recovery'], $_GET['user_id'] ) ) {
+ return false;
+ }
+
+ if ( ! $this->is_valid_protect_recovery_key( $_GET['validate_jetpack_protect_recovery'], $_GET['user_id'] ) ) {
+ return false;
+ }
+
+ $this->valid_blocked_user_id = (int) $_GET['user_id'];
+
+ return true;
+ }
+
+ public function is_valid_protect_recovery_key( $key, $user_id ) {
+
+ $path = sprintf( '/sites/%d/protect/recovery/confirm', Jetpack::get_option( 'id' ) );
+ $response = Jetpack_Client::wpcom_json_api_request_as_blog(
+ $path,
+ '1.1',
+ array(
+ 'method' => 'post'
+ ),
+ array(
+ 'token' => $key,
+ 'user_id' => $user_id,
+ 'ip' => $this->ip_address,
+ )
+ );
+
+ $result = json_decode( wp_remote_retrieve_body( $response ) );
+
+ if ( is_wp_error( $result ) || empty( $result ) || isset( $result->error ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function render_and_die() {
+ if ( ! $this->can_send_recovery_emails ) {
+ $this->render_blocked_login_message();
+
+ return;
+ }
+
+ if ( isset( $_GET['validate_jetpack_protect_recovery'] ) && $_GET['user_id'] ) {
+ $error = new WP_Error( 'invalid_token', __( "Oops, we couldn't validate the recovery token.", 'jetpack' ) );
+ $this->protect_die( $error );
+
+ return;
+ }
+
+ if (
+ isset( $_GET['jetpack-protect-recovery'] ) &&
+ isset( $_POST['_wpnonce'] ) &&
+ wp_verify_nonce( $_POST['_wpnonce'], 'bypass-protect' )
+ ) {
+ $this->process_recovery_email();
+
+ return;
+ }
+
+ if ( isset( $_GET['loggedout'] ) && 'true' === $_GET['loggedout'] ) {
+ $this->protect_die( __( 'You successfully logged out.', 'jetpack' ) );
+ }
+
+ $this->render_recovery_form();
+ }
+
+ public function render_blocked_login_message() {
+ $this->protect_die( $this->get_html_blocked_login_message() );
+ }
+
+ function process_recovery_email() {
+ $sent = $this->send_recovery_email();
+ $show_recovery_form = true;
+ if ( is_wp_error( $sent ) ) {
+ if ( 'email_already_sent' === $sent->get_error_code() ) {
+ $show_recovery_form = false;
+ }
+ $this->protect_die( $sent,null,true, $show_recovery_form );
+ } else {
+ $this->render_recovery_success();
+ }
+ }
+
+ function send_recovery_email() {
+ $email = isset( $_POST['email'] ) ? $_POST['email'] : '';
+ if ( sanitize_email( $email ) !== $email || ! is_email( $email ) ) {
+ return new WP_Error( 'invalid_email', __( "Oops, looks like that's not the right email address. Please try again!", 'jetpack' ) );
+ }
+ $user = get_user_by( 'email', trim( $email ) );
+
+ if ( ! $user ) {
+ return new WP_Error( 'invalid_user', __( "Oops, we couldn't find a user with that email. Please try again!", 'jetpack' ) );
+ }
+ $this->email_address = $email;
+ $path = sprintf( '/sites/%d/protect/recovery/request', Jetpack::get_option( 'id' ) );
+
+
+ $response = Jetpack_Client::wpcom_json_api_request_as_blog(
+ $path,
+ '1.1',
+ array(
+ 'method' => 'post'
+ ),
+ array(
+ 'user_id' => $user->ID,
+ 'ip' => $this->ip_address
+ )
+ );
+
+ $code = wp_remote_retrieve_response_code( $response );
+ $result = json_decode( wp_remote_retrieve_body( $response ) );
+
+ if ( self::HTTP_STATUS_CODE_TOO_MANY_REQUESTS === $code ) {
+ return new WP_Error( 'email_already_sent', sprintf( __( 'Recovery instructions were sent to %s. Check your inbox!', 'jetpack' ), $this->email_address ) );
+ } else if ( is_wp_error( $result ) || empty( $result ) || isset( $result->error ) ) {
+ return new WP_Error( 'email_send_error', __( 'Oops, we were unable to send a recovery email. Try again.', 'jetpack' ) );
+ }
+
+ return true;
+ }
+
+ function protect_die( $content, $title = null, $back_link = false, $recovery_form = false ) {
+ if ( empty( $title ) ) {
+ $title = __( 'Jetpack has locked your site\'s login page.', 'jetpack' );
+ }
+ if ( is_wp_error( $content ) ) {
+ $svg = '<svg class="gridicon gridicons-notice" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 15h-2v-2h2v2zm0-4h-2l-.5-6h3l-.5 6z"/></g></svg>';
+ $content = '<span class="error"> '. $svg . $content->get_error_message() . '</span>';
+ }
+ $content = '<p>'. $content .'</p>';
+
+ // If for some reason the login pop up box show up in the wp-admin.
+ if ( isset( $_GET['interim-login'] ) ) {
+ $content = "<style>html{ background-color: #fff; } #error-message { margin:0 auto; padding: 1em; box-shadow: none; } </style>" . $content;
+ }
+ $this->display_page( $title, $content, $back_link, $recovery_form );
+
+ }
+
+ function render_recovery_form() {
+ $content = $this->get_html_blocked_login_message();
+ $this->protect_die( $content, null, null, true );
+ }
+
+ function render_recovery_success() {
+ $this->protect_die( sprintf( __( 'Recovery instructions were sent to %s. Check your inbox!', 'jetpack' ), $this->email_address ) );
+ }
+
+
+ function get_html_blocked_login_message() {
+ $icon = '<svg class="gridicon gridicons-spam" style="fill:#d94f4f" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M17 2H7L2 7v10l5 5h10l5-5V7l-5-5zm-4 15h-2v-2h2v2zm0-4h-2l-.5-6h3l-.5 6z"/></g></svg>';
+ $ip = str_replace( 'http://', '', esc_url( 'http://' . $this->ip_address ) );
+ return sprintf(
+ __( '<p>Your IP address <code>%2$s</code> has been flagged for potential security violations. You can unlock your login by sending yourself a special link via email. <a href="%3$s">Learn More</a></p>', 'jetpack' ),
+ $icon,
+ $ip,
+ esc_url( self::HELP_URL )
+ );
+ }
+
+ function get_html_recovery_form() {
+ ob_start(); ?>
+ <div>
+ <form method="post" action="?jetpack-protect-recovery=true">
+ <?php echo wp_nonce_field( 'bypass-protect' ); ?>
+ <p><label for="email"><?php esc_html_e( 'Your email', 'jetpack' ); ?><br/></label>
+ <input type="email" name="email" class="text-input"/>
+ <input type="submit" class="button"
+ value="<?php esc_attr_e( 'Send email', 'jetpack' ); ?>"/>
+ </p>
+ </form>
+ </div>
+
+ <?php
+ $contents = ob_get_contents();
+ ob_end_clean();
+
+ return $contents;
+ }
+
+ function display_page( $title, $message, $back_button = false, $recovery_form = false ) {
+
+ if ( ! headers_sent() ) {
+ nocache_headers();
+ header( 'Content-Type: text/html; charset=utf-8' );
+ }
+
+ $text_direction = 'ltr';
+ if ( is_rtl() ) {
+ $text_direction = 'rtl';
+ }
+ ?>
+ <!DOCTYPE html>
+ <html xmlns="http://www.w3.org/1999/xhtml" <?php if ( function_exists( 'language_attributes' ) && function_exists( 'is_rtl' ) ) {
+ language_attributes();
+ } else {
+ echo "dir='$text_direction'";
+ } ?>>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width">
+ <?php
+ if ( function_exists( 'wp_no_robots' ) ) {
+ wp_no_robots();
+ }
+ ?>
+ <title><?php echo $title ?></title>
+ <style type="text/css">
+ html {
+ background: #f6f6f6;
+ }
+
+ body {
+ color: #2e4453;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+ margin: 2em auto;
+ padding: 1em 2em;
+ max-width: 460px;
+ text-align: left;
+ }
+ body.is-rtl {
+ text-align: right;
+ }
+ h1 {
+ clear: both;
+ color: #3d596d;
+ font-size: 24px;
+ margin:0 0 24px 0;
+ padding: 0;
+ font-weight: 400;
+ }
+
+ #error-message {
+ box-sizing: border-box;
+ background: white;
+ box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
+ padding: 24px;
+ }
+
+ #error-message img {
+ margin: 0 auto;
+ display: block;
+ }
+
+ #error-page {
+ margin-top: 50px;
+ }
+
+ #error-page p {
+ font-size: 14px;
+ line-height: 1.5;
+ margin: 24px 0 0;
+ }
+
+ #error-page code {
+ font-family: Consolas, Monaco, monospace;
+ }
+
+ ul li {
+ margin-bottom: 10px;
+ font-size: 14px;
+ }
+
+ a {
+ color: #00aadc;
+ }
+
+ label {
+ font-weight: bold;
+ font-size:16px;
+ }
+
+ a:hover,
+ a:active {
+ color: #0085be;
+ }
+
+ a:focus {
+ color: #124964;
+ -webkit-box-shadow: 0 0 0 1px #5b9dd9,
+ 0 0 2px 1px rgba(30, 140, 190, .8);
+ box-shadow: 0 0 0 1px #5b9dd9,
+ 0 0 2px 1px rgba(30, 140, 190, .8);
+ outline: none;
+ }
+
+ .button {
+ background: #00aadc;
+ color: white;
+ border-color: #008ab3;
+ border-style: solid;
+ border-width: 1px 1px 2px;
+ cursor: pointer;
+ display: inline-block;
+ margin: 0;
+ margin-right: 0px;
+ outline: 0;
+ overflow: hidden;
+ font-weight: 500;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ box-sizing: border-box;
+ font-size: 14px;
+ line-height: 21px;
+ border-radius: 4px;
+ padding: 7px 14px 9px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ font-size: 14px;
+ width: 100%;
+ }
+
+ .button:hover,
+ .button:focus {
+ border-color: #005082;
+ outline: none;
+ }
+
+ .button:focus {
+ border-color: #005082;
+ -webkit-box-shadow: 0 0 3px rgba(0, 115, 170, .8);
+ box-shadow: 0 0 3px rgba(0, 115, 170, .8);
+ outline: none;
+ }
+ .button::-moz-focus-inner {
+ border: 0;
+ }
+
+ .button:active {
+ border-width: 2px 1px 1px;
+ }
+ .gridicon {
+ fill: currentColor;
+ vertical-align: middle;
+ }
+ #error-footer {
+ padding: 16px;
+ }
+ #error-footer a {
+ text-decoration: none;
+ line-height:20px;
+ font-size: 14px;
+ color: #4f748e;
+ }
+ #error-footer a:hover {
+ color: #2e4453;
+ }
+ #error-footer .gridicon{
+ width: 16px;
+ }
+ #error-footer .gridicons-help {
+ width: 24px;
+ margin-right:8px;
+ }
+
+ .is-rtl #error-footer .gridicons-help {
+ margin-left:8px;
+ }
+
+ .error {
+ background: #d94f4f;
+ color:#FFF;
+ display: block;
+ border-radius: 3px;
+ line-height: 1.5;
+ padding: 16px;
+ padding-left: 42px;
+ }
+ .is-rtl .error {
+ padding-right: 42px;
+ }
+ .error .gridicon {
+ float: left;
+ margin-left: -32px;
+ }
+
+ .is-rtl .error .gridicon {
+ float: right;
+ margin-right: -32px;
+ }
+
+ .text-input {
+ margin: 0;
+ padding: 7px 14px;
+ width: 100%;
+ color: #2e4453;
+ font-size: 16px;
+ line-height: 1.5;
+ border: 1px solid #c8d7e1;
+ background-color: white;
+ transition: all .15s ease-in-out;
+ box-sizing: border-box;
+ margin: 8px 0 16px;
+ }
+ #image {
+ display: block;
+ width: 200px;
+ margin: 0 auto;
+ }
+ <?php
+ $rtl_class = '';
+ if ( 'rtl' == $text_direction ) {
+ $rtl_class = 'class="is-rtl"';
+ echo 'body { font-family: Tahoma, Arial; }';
+ }
+ ?>
+ </style>
+ </head>
+ <body id="error-page" <?php echo $rtl_class; ?>>
+ <h1 id="error-title"><?php echo esc_html( $title ); ?></h1>
+ <div id="error-message">
+ <svg id="image" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 250 134">
+ <path fill="#E9EFF4" d="M205.2,129.8c3.7-0.7,7.4-0.9,11.1-1.1l5.5-0.1l5.5,0c3.7,0,7.4,0.1,11.1,0.2c3.7,0.1,7.4,0.3,11.1,0.8 c0.3,0,0.5,0.3,0.5,0.6c0,0.2-0.2,0.4-0.5,0.5c-3.7,0.5-7.4,0.6-11.1,0.8c-3.7,0.1-7.4,0.2-11.1,0.2l-5.5,0l-5.5-0.1 c-3.7-0.1-7.4-0.4-11.1-1.1c-0.1,0-0.2-0.2-0.2-0.3C205,129.9,205.1,129.8,205.2,129.8"/>
+ <path fill="#E9EFF4" d="M0.2,130.9c3-0.7,5.9-0.9,8.9-1.1l4.4-0.1l4.4,0c3,0,5.9,0.1,8.9,0.2c3,0.1,5.9,0.3,8.9,0.8 c0.3,0,0.5,0.3,0.4,0.6c0,0.2-0.2,0.4-0.4,0.4c-3,0.5-5.9,0.6-8.9,0.8c-3,0.1-5.9,0.2-8.9,0.2l-4.4,0l-4.4-0.1 c-3-0.1-5.9-0.4-8.9-1.1c-0.1,0-0.2-0.2-0.2-0.3C0,131,0.1,130.9,0.2,130.9"/>
+ <path fill="#C8D7E2" d="M101.6,130.1H70.1V52.5c0-8.5,6.9-15.3,15.3-15.3h16.1V130.1z"/>
+ <path fill="#0DA9DD" d="M191.5,130.1h-73.8v-5.4c0-8.9,7.2-16.1,16.1-16.1h57.7V130.1z"/>
+ <path fill="#C7E9F5" d="M55.2,25.6l-0.1,9.8L55,57l-0.1,21.6c0,0.2,0.2,0.4,0.4,0.4c0.2,0,0.4-0.2,0.4-0.4L56.6,57l0.8-21.6 c0.1-3.3,0.2-6.5,0.3-9.8H55.2z"/>
+ <path fill="#C7E9F5" d="M203.1,25.6l0.1,18.1c0.2,28.8,0.4,57.6,1.2,86.3c0,0.4,0.4,0.8,0.8,0.8c0.4,0,0.8-0.3,0.8-0.8 c0.8-28.8,1-57.6,1.2-86.3l0.1-18.1H203.1z"/>
+ <path fill="#7FD3F2" d="M55.3,25.6v-8.2v-6.8c0-5.9,4-10.7,9-10.7h134c5,0,9,4.8,9,10.7v14.9H55.3z"/>
+ <path fill="#005083" d="M210.7,25.6c-13.3,1.1-26.7,1-40,1l-40,0.2l-40-0.2c-13.3-0.1-26.7,0-40-1V25c13.3-1.1,26.7-1,40-1l40-0.2 l40,0.2c13.3,0.1,26.7,0,40,1V25.6z"/>
+ <polygon fill="#C7E9F5" points="168.7,95.6 117.7,95.6 117.7,44.6 "/>
+ <path fill="#C8D7E2" d="M191.5,56.5c0,11-8.9,19.9-19.9,19.9c-11,0-19.9-8.9-19.9-19.9c0-11,8.9-19.9,19.9-19.9 C182.6,36.6,191.5,45.5,191.5,56.5"/>
+ <path fill="#FFFFFF" d="M213.2,95.5c-3.3-5.1-3.2-16.7-3.2-28.4h-32.3c0,0-5.2,25.5,4.6,33c7.5-0.1,29.9-0.6,29.9-0.6"/>
+ <path fill="#C8D7E2" d="M213.5,95.3l-0.1-0.1l-0.3-0.5c-0.2-0.4-0.3-0.7-0.5-1.1c-0.3-0.8-0.5-1.6-0.7-2.4c-0.1-0.5-0.2-1.1-0.3-1.6 c-0.4,0-0.8,0-1.2,0c0.5,2.1,1.1,4.3,2.4,6.1l0.2,0.2c0.2,0,0.4-0.1,0.5-0.3C213.6,95.5,213.6,95.4,213.5,95.3L213.5,95.3z"/>
+ <path fill="#C8D7E2" d="M212.5,98.6c-0.1,0-0.2,0-0.3,0l-0.1,0H212l-0.3,0l-0.6,0l-1.3,0l-2.5,0l-5,0l-19.5,0.2 c-1.9-1.7-3.1-4.1-3.8-6.5c-0.8-2.6-1.1-5.4-1.2-8.2c-0.2-5.2,0.3-10.4,1.1-15.6l5.7-0.1c0-0.9,0-1.8,0-2.6l-4.4,0l-2.5,0 c-0.4,0-0.8,0.2-1,0.5c-0.1,0.2-0.2,0.3-0.3,0.5l-0.1,0.3l-0.2,1.2c-0.3,1.7-0.5,3.3-0.7,5c-0.3,3.3-0.5,6.7-0.4,10.1 c0.1,3.4,0.5,6.7,1.5,10c0.5,1.6,1.2,3.2,2.2,4.7c0.5,0.7,1,1.4,1.7,2c0.3,0.3,0.6,0.6,1,0.9l0.1,0.1c0.1,0,0.2,0.1,0.3,0.2 c0.2,0.1,0.5,0.1,0.6,0.1l0.6,0l20-0.6l5-0.2l2.5-0.1l1.2,0l0.3,0l0.2,0c0,0,0.3,0,0.4-0.1c0.3-0.2,0.5-0.5,0.5-0.9 C213.1,99.1,212.9,98.7,212.5,98.6z"/>
+ <path fill="#FFFFFF" d="M223.1,84.8c-3.3-5.1-4.8-16.7-4.8-28.4h-32.3c0,0-3.5,25.5,6.3,33c7.5-0.1,29.9-0.6,29.9-0.6"/>
+ <path fill="#C8D7E2" d="M222.9,84.9c-1.3-2.1-2.2-4.4-2.8-6.7c-0.6-2.4-1.1-4.8-1.5-7.2c-0.7-4.8-1-9.1-1-13.9l0,0l-31,0.1l0,0 c-0.4,2.8-0.5,5.1-0.5,7.9c-0.1,2.9,0,5.7,0.3,8.6c0.3,2.8,0.8,5.7,1.7,8.3c0.9,2.6,2.3,5.2,4.5,6.9l-0.4-0.1l14.9-0.2 c5-0.1,10-0.1,14.9-0.1c0.1,0,0.3,0.1,0.3,0.3c0,0.1-0.1,0.3-0.2,0.3c-5,0.2-10,0.4-14.9,0.5l-14.9,0.4c-0.1,0-0.3,0-0.4-0.1l0,0 c-2.5-1.9-3.9-4.7-5-7.4c-1-2.8-1.5-5.7-1.9-8.6c-0.3-2.9-0.4-5.8-0.4-8.8c0.1-2.9,0.2-5.8,0.6-8.8c0-0.4,0.4-0.6,0.7-0.6h0 l32.3,0.1h0c0.3,0,0.6,0.3,0.6,0.6v0c0,4.8,0.2,9.6,0.7,14.4c0.3,2.4,0.6,4.8,1.2,7.1c0.5,2.3,1.2,4.7,2.4,6.8c0,0.1,0,0.1,0,0.2 C223.1,85,223,85,222.9,84.9"/>
+ <path fill="#C8D7E2" d="M192.1,67.1c1.6-0.9,3.4-1.2,5.1-1.3c1.7-0.2,3.5-0.2,5.2-0.2c3.5,0.1,6.9,0.2,10.3,1c0.1,0,0.2,0.2,0.2,0.3 c0,0.1-0.1,0.2-0.2,0.2c-3.4,0.2-6.9,0-10.3,0c-1.7,0-3.4,0-5.1,0c-1.7,0-3.4,0.1-5.1,0.3l0,0c-0.1,0-0.1,0-0.1-0.1 C192,67.2,192.1,67.1,192.1,67.1"/>
+ <path fill="#C8D7E2" d="M194.1,74c1.4,0,2.7,0,4.1,0c1.4,0,2.7,0,4.1,0c2.7,0,5.4-0.1,8.2-0.2c0.1,0,0.3,0.1,0.3,0.3 c0,0.1-0.1,0.2-0.2,0.3c-1.3,0.5-2.7,0.7-4.1,0.9c-1.4,0.2-2.8,0.2-4.2,0.3c-1.4,0-2.8,0-4.2-0.2c-1.4-0.2-2.8-0.4-4.1-1.1 c-0.1,0-0.1-0.1,0-0.2C193.9,74.1,194,74,194.1,74L194.1,74z"/>
+ <path fill="#86A6BD" d="M40.2,88.6c-0.5,0-0.8-0.4-0.9-0.9l-0.1-8.2c0-0.7,0-1.4,0-2.1c0.1-0.7,0.2-1.5,0.4-2.2c0.4-1.4,1-2.8,1.9-4 c1.7-2.5,4.3-4.3,7.1-5.1c0.7-0.2,1.5-0.3,2.2-0.5c0.7-0.1,1.5-0.1,2.2-0.1c1.3,0,2.9,0,4.4,0.4c2.9,0.7,5.6,2.5,7.4,4.9 c0.9,1.2,1.6,2.6,2.1,4c0.5,1.4,0.6,3,0.6,4.4l0,16.4c0,0.7-0.6,1.3-1.3,1.3l-6.7,0c-0.7,0-1.3-0.6-1.3-1.3v0l0-10.8l0-5.4 c0-1.4-0.7-2.8-1.8-3.5c-0.6-0.4-1.3-0.6-2-0.7c-0.7,0-1.9,0-2.5,0c-1.4,0.1-2.7,1-3.3,2.3c-0.3,0.7-0.4,1.3-0.4,2.1l0,2.7 l-0.1,5.4l0,0c0,0.5-0.4,0.9-1,0.9"/>
+ <path fill="#FFFFFF" d="M41.1,86.9l0.1-7.3c-0.1-2.6,0.7-5,2.1-7.1c1.4-2,3.6-3.5,5.9-4.1c0.6-0.2,1.2-0.3,1.8-0.3 c0.6,0,1.2-0.1,1.9,0c1.4,0,2.5,0,3.7,0.4c2.4,0.6,4.5,2,5.9,4c0.7,1,1.3,2.1,1.6,3.2c0.4,1.2,0.5,2.3,0.5,3.7l0,15.1l0,0l-4.2,0 l0-9.5l0-5.4c0-2.2-1.2-4.4-3-5.5c-0.9-0.6-2-0.9-3.1-1c-1.1,0-1.7,0-2.9,0c-2.2,0.2-4.2,1.7-5.1,3.6c-0.5,0.9-0.7,2.1-0.6,3.1 l0,2.7l0.1,4.4l0,0L41.1,86.9L41.1,86.9"/>
+ <path fill="#86A6BD" d="M36.3,133c-1.9,0-3.8-1.1-4.8-2.8c-0.5-0.8-0.7-1.8-0.7-2.8l0-2.4l0-9.6l-0.1-9.6l0-4.8c0-0.7,0-1.8,0.3-2.8 c0.3-1,0.9-1.8,1.7-2.5c0.8-0.6,1.7-1.1,2.7-1.3c1.1-0.2,1.8-0.1,2.6-0.1l4.8,0l9.6-0.1l19.2,0c2.1,0,4.1,1.2,5.1,3 c0.5,0.9,0.8,2,0.8,3l0,2.4l0,9.6l-0.1,9.6l0,4.8c0,0.7,0,1.8-0.4,2.8c-0.3,0.9-1,1.8-1.7,2.4c-0.8,0.6-1.7,1.1-2.7,1.2 c-1.1,0.1-1.8,0-2.6,0.1l-4.8,0l-9.6-0.1L36.3,133z"/>
+ <path fill="#FFFFFF" d="M74.8,112.3l-0.1-9.6l0-2.4c0-0.6-0.1-1.1-0.4-1.6c-0.6-1-1.7-1.6-2.8-1.6l-19.2,0L42.7,97l-4.8,0 c-0.8,0-1.7,0-2.2,0c-0.6,0.1-1.1,0.3-1.6,0.7c-0.5,0.4-0.8,0.9-1,1.4c-0.2,0.6-0.2,1.1-0.2,2l0,4.8l-0.1,9.6l0,9.6l0,2.4 c0,0.6,0.2,1.3,0.5,1.8c0.6,1.1,1.9,1.8,3.1,1.8l19.2-0.1l9.6-0.1l4.8,0c0.8,0,1.7,0,2.2-0.1c0.6-0.1,1.2-0.4,1.6-0.8 c0.5-0.4,0.8-0.9,1-1.5c0.2-0.6,0.2-1.1,0.2-2l0-4.8L74.8,112.3z"/>
+ <path fill="#86A6BD" d="M48.1,121.4l2.9-6.2c0.3-0.6,0.2-1.3-0.3-1.8c-1-1-1.5-2.5-1.2-4c0.3-1.7,1.7-3.1,3.4-3.4 c2.9-0.6,5.4,1.6,5.4,4.4c0,1.2-0.5,2.3-1.3,3.1c-0.5,0.5-0.6,1.2-0.3,1.8l2.9,6.2c0.1,0.2-0.1,0.5-0.3,0.5H48.4 C48.1,121.9,48,121.6,48.1,121.4"/>
+ </svg>
+
+ <?php echo $message; ?>
+ <?php if ( $recovery_form ) {
+ echo $this->get_html_recovery_form();
+ } ?>
+ </div>
+ <div id="error-footer">
+ <?php if ( $back_button && ! $recovery_form ) {
+ if ( 'rtl' == $text_direction ) {
+ $back_button_icon = '<svg class="gridicon gridicons-arrow-right" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></g></svg>';
+ } else {
+ $back_button_icon = '<svg class="gridicon gridicons-arrow-left" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></g></svg>';
+ }
+ ?>
+ <a href='javascript:history.back()'><?php printf( __( '%s Back' ), $back_button_icon ); ?></a>
+ <?php } else {
+ $help_icon = '<svg class="gridicon gridicons-help" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 16h-2v-2h2v2zm0-4.14V15h-2v-2c0-.552.448-1 1-1 1.103 0 2-.897 2-2s-.897-2-2-2-2 .897-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.862-1.278 3.413-3 3.86z"/></g></svg>';?>
+ <a href="<?php echo esc_url( self::HELP_URL ); ?>" rel="noopener noreferrer" target="_blank"><?php printf( __( '%s Get help unlocking your site' ), $help_icon );?></a>
+ <?php } ?>
+ </div>
+ </body>
+ </html>
+ <?php
+ die();
+ }
+}
diff --git a/plugins/jetpack/modules/protect/math-fallback.php b/plugins/jetpack/modules/protect/math-fallback.php
new file mode 100644
index 00000000..bc29fa86
--- /dev/null
+++ b/plugins/jetpack/modules/protect/math-fallback.php
@@ -0,0 +1,158 @@
+<?php
+
+if ( ! class_exists( 'Jetpack_Protect_Math_Authenticate' ) ) {
+ /*
+ * The math captcha fallback if we can't talk to the Protect API
+ */
+ class Jetpack_Protect_Math_Authenticate {
+
+ static $loaded;
+
+ function __construct() {
+
+ if ( self::$loaded ) {
+ return;
+ }
+
+ self::$loaded = 1;
+
+ add_action( 'login_form', array( $this, 'math_form' ) );
+
+ if( isset( $_POST[ 'jetpack_protect_process_math_form' ] ) ) {
+ add_action( 'init', array( $this, 'process_generate_math_page' ) );
+ }
+ }
+
+ private static function time_window() {
+ return ceil( time() / ( MINUTE_IN_SECONDS * 2 ) );
+ }
+
+ /**
+ * Verifies that a user answered the math problem correctly while logging in.
+ *
+ * @return bool Returns true if the math is correct
+ * @throws Error if insuffient $_POST variables are present.
+ * @throws Error message if the math is wrong
+ */
+ static function math_authenticate() {
+ if( isset( $_COOKIE[ 'jpp_math_pass' ] ) ) {
+ $jetpack_protect = Jetpack_Protect_Module::instance();
+ $transient = $jetpack_protect->get_transient( 'jpp_math_pass_' . $_COOKIE[ 'jpp_math_pass' ] );
+
+ if( !$transient || $transient < 1 ) {
+ Jetpack_Protect_Math_Authenticate::generate_math_page();
+ }
+ return true;
+ }
+
+ $ans = isset( $_POST['jetpack_protect_num'] ) ? (int) $_POST['jetpack_protect_num'] : '' ;
+ $correct_ans = isset( $_POST[ 'jetpack_protect_answer' ] ) ? $_POST[ 'jetpack_protect_answer' ] : '' ;
+
+ $time_window = Jetpack_Protect_Math_Authenticate::time_window();
+ $salt = get_site_option( 'jetpack_protect_key' ) . '|' . get_site_option( 'admin_email' ) . '|';
+ $salted_ans_1 = hash_hmac( 'sha1', $ans, $salt . $time_window );
+ $salted_ans_2 = hash_hmac( 'sha1', $ans, $salt . ( $time_window - 1 ) );
+
+ if ( ! $correct_ans || ! $ans ) {
+ Jetpack_Protect_Math_Authenticate::generate_math_page();
+ } elseif ( ! hash_equals( $salted_ans_1, $correct_ans ) && ! hash_equals( $salted_ans_2, $correct_ans ) ) {
+ wp_die(
+ __( '<strong>You failed to correctly answer the math problem.</strong> This is used to combat spam when the Protect API is unavailable. Please use your browser\'s back button to return to the login form, press the "refresh" button to generate a new math problem, and try to log in again.', 'jetpack' ),
+ '',
+ array ( 'response' => 401 )
+ );
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Creates an interim page to collect answers to a math captcha
+ *
+ * @return none, execution stopped
+ */
+ static function generate_math_page( $error = false ) {
+ ob_start();
+ ?>
+ <h2><?php esc_html_e( 'Please solve this math problem to prove that you are not a bot. Once you solve it, you will need to log in again.', 'jetpack' ); ?></h2>
+ <?php if ($error): ?>
+ <h3><?php esc_html_e( 'Your answer was incorrect, please try again.', 'jetpack' ); ?></h3>
+ <?php endif ?>
+
+ <form action="<?php echo wp_login_url(); ?>" method="post" accept-charset="utf-8">
+ <?php Jetpack_Protect_Math_Authenticate::math_form(); ?>
+ <input type="hidden" name="jetpack_protect_process_math_form" value="1" id="jetpack_protect_process_math_form" />
+ <p><input type="submit" value="<?php esc_attr_e( 'Continue &rarr;', 'jetpack' ); ?>"></p>
+ </form>
+ <?php
+ $mathpage = ob_get_contents();
+ ob_end_clean();
+ wp_die(
+ $mathpage,
+ '',
+ array ( 'response' => 401 )
+ );
+ }
+
+ public function process_generate_math_page() {
+ $ans = isset( $_POST['jetpack_protect_num'] ) ? (int)$_POST['jetpack_protect_num'] : '';
+ $correct_ans = isset( $_POST[ 'jetpack_protect_answer' ] ) ? $_POST[ 'jetpack_protect_answer' ] : '' ;
+
+ $time_window = Jetpack_Protect_Math_Authenticate::time_window();
+ $salt = get_site_option( 'jetpack_protect_key' ) . '|' . get_site_option( 'admin_email' ) . '|';
+ $salted_ans_1 = hash_hmac( 'sha1', $ans, $salt . $time_window );
+ $salted_ans_2 = hash_hmac( 'sha1', $ans, $salt . ( $time_window - 1 ) );
+
+ if ( ! hash_equals( $salted_ans_1, $correct_ans ) && ! hash_equals( $salted_ans_2, $correct_ans ) ) {
+ Jetpack_Protect_Math_Authenticate::generate_math_page(true);
+ } else {
+ $temp_pass = substr( hash_hmac( 'sha1', rand( 1, 100000000 ), get_site_option( 'jetpack_protect_key' ) ), 5, 25 );
+
+ $jetpack_protect = Jetpack_Protect_Module::instance();
+ $jetpack_protect->set_transient( 'jpp_math_pass_' . $temp_pass, 3, DAY_IN_SECONDS );
+ setcookie('jpp_math_pass', $temp_pass, time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false);
+ remove_action( 'login_form', array( $this, 'math_form' ) );
+ return true;
+ }
+ }
+
+ /**
+ * Requires a user to solve a simple equation. Added to any WordPress login form.
+ *
+ * @return VOID outputs html
+ */
+ static function math_form() {
+ // Check if jpp_math_pass cookie is set and it matches valid transient
+ if( isset( $_COOKIE[ 'jpp_math_pass' ] ) ) {
+ $jetpack_protect = Jetpack_Protect_Module::instance();
+ $transient = $jetpack_protect->get_transient( 'jpp_math_pass_' . $_COOKIE[ 'jpp_math_pass' ] );
+
+ if( $transient && $transient > 0 ) {
+ return '';
+ }
+ }
+
+ $num1 = rand( 0, 10 );
+ $num2 = rand( 1, 10 );
+ $ans = $num1 + $num2;
+
+ $time_window = Jetpack_Protect_Math_Authenticate::time_window();
+ $salt = get_site_option( 'jetpack_protect_key' ) . '|' . get_site_option( 'admin_email' ) . '|';
+ $salted_ans = hash_hmac( 'sha1', $ans, $salt . $time_window );
+ ?>
+ <div style="margin: 5px 0 20px;">
+ <label for="jetpack_protect_answer">
+ <?php esc_html_e( 'Prove your humanity', 'jetpack' ); ?>
+ </label>
+ <br/>
+ <span style="vertical-align:super;">
+ <?php echo esc_html( "$num1 &nbsp; + &nbsp; $num2 &nbsp; = &nbsp;" ); ?>
+ </span>
+ <input type="text" id="jetpack_protect_answer" name="jetpack_protect_num" value="" size="2" style="width:30px;height:25px;vertical-align:middle;font-size:13px;" class="input" />
+ <input type="hidden" name="jetpack_protect_answer" value="<?php echo esc_attr( $salted_ans ); ?>" />
+ </div>
+ <?php
+ }
+
+ }
+}
diff --git a/plugins/jetpack/modules/protect/protect-dashboard-widget-rtl.css b/plugins/jetpack/modules/protect/protect-dashboard-widget-rtl.css
new file mode 100644
index 00000000..20bc0893
--- /dev/null
+++ b/plugins/jetpack/modules/protect/protect-dashboard-widget-rtl.css
@@ -0,0 +1,117 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+/* loads inline on wp-admin in order to reduce http requests */
+
+#protect_dashboard_widget .inside {
+ margin: 0;
+ padding: 0;
+ text-align: center;
+}
+
+.jetpack-security * {
+ box-sizing: border-box;
+}
+
+/* alert msgs */
+#protect_dashboard_widget .msg {
+ color: #fff;
+ text-align: center;
+ padding: 10px;
+}
+
+#protect_dashboard_widget .msg.working {
+ background: #7BAC48;
+}
+
+#protect_dashboard_widget .msg.attn {
+ background: #d94f4f;
+}
+
+#protect_dashboard_widget .msg a {
+ color: #fff;
+ text-decoration: underline;
+}
+
+#protect_dashboard_widget .msg a:hover {
+ text-decoration: none;
+}
+
+#protect_dashboard_widget .msg .dashicons {
+ float: left;
+ text-decoration: none;
+ border-radius: 2px;
+}
+
+#protect_dashboard_widget .msg.working .dashicons {
+ color: #609643;
+}
+
+#protect_dashboard_widget .msg.working .dashicons:hover {
+ background: #609643;
+ color: #7BAC48;
+}
+
+#protect_dashboard_widget .msg.attn .dashicons {
+ color: #a93838;
+}
+
+#protect_dashboard_widget .msg.attn .dashicons:hover {
+ background: #a93838;
+ color: #d94f4f;
+}
+
+.blocked-attacks,
+.file-scanning {
+ position: relative;
+}
+
+.blocked-attacks {
+ background: #fafafa;
+ border-bottom: 1px #eee solid;
+ padding-bottom: 35px;
+}
+
+.jetpack-security-sharing {
+ width: 60px;
+ display: inline-block;
+ position: absolute;
+ left: 0;
+ top: 10px;
+}
+
+.jetpack-security-sharing a {
+ color: #dcdcdc;
+}
+
+.jetpack-security-sharing a:hover {
+ color: #cdcbcb;
+}
+
+.blocked-attacks h2,
+.blocked-attacks h3 {
+ color: #7BAC48;
+ font-weight: 300;
+}
+
+.blocked-attacks h2 {
+ font-size: 4em;
+ line-height: 110%;
+ margin: 0;
+ padding: 10px 12px 10px 12px;
+}
+
+.blocked-attacks h3 {
+ font-size: 1.1em;
+ line-height: 110%;
+ padding: 0 12px 10px 12px;
+ margin: 0;
+}
+
+.jetpack-protect-logo {
+ width: 50px;
+ position: relative;
+}
+
+.file-scanning {
+ margin-top: -30px;
+ padding: 0 12px;
+}
diff --git a/plugins/jetpack/modules/protect/protect-dashboard-widget-rtl.min.css b/plugins/jetpack/modules/protect/protect-dashboard-widget-rtl.min.css
new file mode 100644
index 00000000..72993a8d
--- /dev/null
+++ b/plugins/jetpack/modules/protect/protect-dashboard-widget-rtl.min.css
@@ -0,0 +1 @@
+#protect_dashboard_widget .inside{margin:0;padding:0;text-align:center}.jetpack-security *{box-sizing:border-box}#protect_dashboard_widget .msg{color:#fff;text-align:center;padding:10px}#protect_dashboard_widget .msg.working{background:#7bac48}#protect_dashboard_widget .msg.attn{background:#d94f4f}#protect_dashboard_widget .msg a{color:#fff;text-decoration:underline}#protect_dashboard_widget .msg a:hover{text-decoration:none}#protect_dashboard_widget .msg .dashicons{float:left;text-decoration:none;border-radius:2px}#protect_dashboard_widget .msg.working .dashicons{color:#609643}#protect_dashboard_widget .msg.working .dashicons:hover{background:#609643;color:#7bac48}#protect_dashboard_widget .msg.attn .dashicons{color:#a93838}#protect_dashboard_widget .msg.attn .dashicons:hover{background:#a93838;color:#d94f4f}.blocked-attacks,.file-scanning{position:relative}.blocked-attacks{background:#fafafa;border-bottom:1px #eee solid;padding-bottom:35px}.jetpack-security-sharing{width:60px;display:inline-block;position:absolute;left:0;top:10px}.jetpack-security-sharing a{color:#dcdcdc}.jetpack-security-sharing a:hover{color:#cdcbcb}.blocked-attacks h2,.blocked-attacks h3{color:#7bac48;font-weight:300}.blocked-attacks h2{font-size:4em;line-height:110%;margin:0;padding:10px 12px 10px 12px}.blocked-attacks h3{font-size:1.1em;line-height:110%;padding:0 12px 10px 12px;margin:0}.jetpack-protect-logo{width:50px;position:relative}.file-scanning{margin-top:-30px;padding:0 12px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/protect/protect-dashboard-widget.css b/plugins/jetpack/modules/protect/protect-dashboard-widget.css
new file mode 100644
index 00000000..b4a6b08e
--- /dev/null
+++ b/plugins/jetpack/modules/protect/protect-dashboard-widget.css
@@ -0,0 +1,116 @@
+/* loads inline on wp-admin in order to reduce http requests */
+
+#protect_dashboard_widget .inside {
+ margin: 0;
+ padding: 0;
+ text-align: center;
+}
+
+.jetpack-security * {
+ box-sizing: border-box;
+}
+
+/* alert msgs */
+#protect_dashboard_widget .msg {
+ color: #fff;
+ text-align: center;
+ padding: 10px;
+}
+
+#protect_dashboard_widget .msg.working {
+ background: #7BAC48;
+}
+
+#protect_dashboard_widget .msg.attn {
+ background: #d94f4f;
+}
+
+#protect_dashboard_widget .msg a {
+ color: #fff;
+ text-decoration: underline;
+}
+
+#protect_dashboard_widget .msg a:hover {
+ text-decoration: none;
+}
+
+#protect_dashboard_widget .msg .dashicons {
+ float: right;
+ text-decoration: none;
+ border-radius: 2px;
+}
+
+#protect_dashboard_widget .msg.working .dashicons {
+ color: #609643;
+}
+
+#protect_dashboard_widget .msg.working .dashicons:hover {
+ background: #609643;
+ color: #7BAC48;
+}
+
+#protect_dashboard_widget .msg.attn .dashicons {
+ color: #a93838;
+}
+
+#protect_dashboard_widget .msg.attn .dashicons:hover {
+ background: #a93838;
+ color: #d94f4f;
+}
+
+.blocked-attacks,
+.file-scanning {
+ position: relative;
+}
+
+.blocked-attacks {
+ background: #fafafa;
+ border-bottom: 1px #eee solid;
+ padding-bottom: 35px;
+}
+
+.jetpack-security-sharing {
+ width: 60px;
+ display: inline-block;
+ position: absolute;
+ right: 0;
+ top: 10px;
+}
+
+.jetpack-security-sharing a {
+ color: #dcdcdc;
+}
+
+.jetpack-security-sharing a:hover {
+ color: #cdcbcb;
+}
+
+.blocked-attacks h2,
+.blocked-attacks h3 {
+ color: #7BAC48;
+ font-weight: 300;
+}
+
+.blocked-attacks h2 {
+ font-size: 4em;
+ line-height: 110%;
+ margin: 0;
+ padding: 10px 12px 10px 12px;
+}
+
+.blocked-attacks h3 {
+ font-size: 1.1em;
+ line-height: 110%;
+ padding: 0 12px 10px 12px;
+ margin: 0;
+}
+
+.jetpack-protect-logo {
+ width: 50px;
+ position: relative;
+}
+
+.file-scanning {
+ margin-top: -30px;
+ padding: 0 12px;
+}
diff --git a/plugins/jetpack/modules/protect/protect-dashboard-widget.min.css b/plugins/jetpack/modules/protect/protect-dashboard-widget.min.css
new file mode 100644
index 00000000..d3b0b996
--- /dev/null
+++ b/plugins/jetpack/modules/protect/protect-dashboard-widget.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#protect_dashboard_widget .inside{margin:0;padding:0;text-align:center}.jetpack-security *{box-sizing:border-box}#protect_dashboard_widget .msg{color:#fff;text-align:center;padding:10px}#protect_dashboard_widget .msg.working{background:#7bac48}#protect_dashboard_widget .msg.attn{background:#d94f4f}#protect_dashboard_widget .msg a{color:#fff;text-decoration:underline}#protect_dashboard_widget .msg a:hover{text-decoration:none}#protect_dashboard_widget .msg .dashicons{float:right;text-decoration:none;border-radius:2px}#protect_dashboard_widget .msg.working .dashicons{color:#609643}#protect_dashboard_widget .msg.working .dashicons:hover{background:#609643;color:#7bac48}#protect_dashboard_widget .msg.attn .dashicons{color:#a93838}#protect_dashboard_widget .msg.attn .dashicons:hover{background:#a93838;color:#d94f4f}.blocked-attacks,.file-scanning{position:relative}.blocked-attacks{background:#fafafa;border-bottom:1px #eee solid;padding-bottom:35px}.jetpack-security-sharing{width:60px;display:inline-block;position:absolute;right:0;top:10px}.jetpack-security-sharing a{color:#dcdcdc}.jetpack-security-sharing a:hover{color:#cdcbcb}.blocked-attacks h2,.blocked-attacks h3{color:#7bac48;font-weight:300}.blocked-attacks h2{font-size:4em;line-height:110%;margin:0;padding:10px 12px 10px 12px}.blocked-attacks h3{font-size:1.1em;line-height:110%;padding:0 12px 10px 12px;margin:0}.jetpack-protect-logo{width:50px;position:relative}.file-scanning{margin-top:-30px;padding:0 12px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/protect/protect.png b/plugins/jetpack/modules/protect/protect.png
new file mode 100644
index 00000000..67bcdbad
--- /dev/null
+++ b/plugins/jetpack/modules/protect/protect.png
Binary files differ
diff --git a/plugins/jetpack/modules/protect/shared-functions.php b/plugins/jetpack/modules/protect/shared-functions.php
new file mode 100644
index 00000000..322901d8
--- /dev/null
+++ b/plugins/jetpack/modules/protect/shared-functions.php
@@ -0,0 +1,316 @@
+<?php
+/**
+ * These functions are shared by the Protect module and its related json-endpoints
+ */
+/**
+ * Returns an array of IP objects that will never be blocked by the Protect module
+ *
+ * The array is segmented into a local whitelist which applies only to the current site
+ * and a global whitelist which, for multisite installs, applies to the entire networko
+ *
+ * @return array
+ */
+function jetpack_protect_format_whitelist() {
+ $local_whitelist = jetpack_protect_get_local_whitelist();
+ $formatted = array(
+ 'local' => array(),
+ );
+ foreach ( $local_whitelist as $item ) {
+ if ( $item->range ) {
+ $formatted['local'][] = $item->range_low . ' - ' . $item->range_high;
+ } else {
+ $formatted['local'][] = $item->ip_address;
+ }
+ }
+ if ( is_multisite() && current_user_can( 'manage_network' ) ) {
+ $formatted['global'] = array();
+ $global_whitelist = jetpack_protect_get_global_whitelist();
+ if ( false === $global_whitelist ) {
+ // If the global whitelist has never been set, check for a legacy option set prior to 3.6.
+ $global_whitelist = get_site_option( 'jetpack_protect_whitelist', array() );
+ }
+ foreach ( $global_whitelist as $item ) {
+ if ( $item->range ) {
+ $formatted['global'][] = $item->range_low . ' - ' . $item->range_high;
+ } else {
+ $formatted['global'][] = $item->ip_address;
+ }
+ }
+ }
+ return $formatted;
+}
+/**
+ * Gets the local Protect whitelist
+ *
+ * The 'local' part of the whitelist only really applies to multisite installs,
+ * which can have a network wide whitelist, as well as a local list that applies
+ * only to the current site. On single site installs, there will only be a local
+ * whitelist.
+ *
+ * @return array A list of IP Address objects or an empty array
+ */
+function jetpack_protect_get_local_whitelist() {
+ $whitelist = Jetpack_Options::get_option( 'protect_whitelist' );
+ if ( false === $whitelist ) {
+ // The local whitelist has never been set.
+ if ( is_multisite() ) {
+ // On a multisite, we can check for a legacy site_option that existed prior to v 3.6, or default to an empty array.
+ $whitelist = get_site_option( 'jetpack_protect_whitelist', array() );
+ } else {
+ // On a single site, we can just use an empty array.
+ $whitelist = array();
+ }
+ }
+ return $whitelist;
+}
+
+/**
+ * Get the global, network-wide whitelist
+ *
+ * It will revert to the legacy site_option if jetpack_protect_global_whitelist has never been set.
+ *
+ * @return array
+ */
+function jetpack_protect_get_global_whitelist() {
+ $whitelist = get_site_option( 'jetpack_protect_global_whitelist' );
+ if ( false === $whitelist ) {
+ // The global whitelist has never been set. Check for legacy site_option, or default to an empty array.
+ $whitelist = get_site_option( 'jetpack_protect_whitelist', array() );
+ }
+ return $whitelist;
+}
+
+/**
+ * Jetpack Protect Save Whitelist.
+ *
+ * @access public
+ * @param mixed $whitelist Whitelist.
+ * @param bool $global (default: false) Global.
+ * @return Bool.
+ */
+function jetpack_protect_save_whitelist( $whitelist, $global = false ) {
+ $whitelist_error = false;
+ $new_items = array();
+ if ( ! is_array( $whitelist ) ) {
+ return new WP_Error( 'invalid_parameters', __( 'Expecting an array', 'jetpack' ) );
+ }
+ if ( $global && ! is_multisite() ) {
+ return new WP_Error( 'invalid_parameters', __( 'Cannot use global flag on non-multisites', 'jetpack' ) );
+ }
+ if ( $global && ! current_user_can( 'manage_network' ) ) {
+ return new WP_Error( 'permission_denied', __( 'Only super admins can edit the global whitelist', 'jetpack' ) );
+ }
+ // Validate each item.
+ foreach ( $whitelist as $item ) {
+ $item = trim( $item );
+ if ( empty( $item ) ) {
+ continue;
+ }
+ $range = false;
+ if ( strpos( $item, '-' ) ) {
+ $item = explode( '-', $item );
+ $range = true;
+ }
+ $new_item = new stdClass();
+ $new_item->range = $range;
+ if ( ! empty( $range ) ) {
+ $low = trim( $item[0] );
+ $high = trim( $item[1] );
+ if ( ! filter_var( $low, FILTER_VALIDATE_IP ) || ! filter_var( $high, FILTER_VALIDATE_IP ) ) {
+ $whitelist_error = true;
+ break;
+ }
+ if ( ! jetpack_convert_ip_address( $low ) || ! jetpack_convert_ip_address( $high ) ) {
+ $whitelist_error = true;
+ break;
+ }
+ $new_item->range_low = $low;
+ $new_item->range_high = $high;
+ } else {
+ if ( ! filter_var( $item, FILTER_VALIDATE_IP ) ) {
+ $whitelist_error = true;
+ break;
+ }
+ if ( ! jetpack_convert_ip_address( $item ) ) {
+ $whitelist_error = true;
+ break;
+ }
+ $new_item->ip_address = $item;
+ }
+ $new_items[] = $new_item;
+ } // End item loop.
+ if ( ! empty( $whitelist_error ) ) {
+ return new WP_Error( 'invalid_ip', __( 'One of your IP addresses was not valid.', 'jetpack' ) );
+ }
+ if ( $global ) {
+ update_site_option( 'jetpack_protect_global_whitelist', $new_items );
+ // Once a user has saved their global whitelist, we can permanently remove the legacy option.
+ delete_site_option( 'jetpack_protect_whitelist' );
+ } else {
+ Jetpack_Options::update_option( 'protect_whitelist', $new_items );
+ }
+ return true;
+}
+
+/**
+ * Jetpack Protect Get IP.
+ *
+ * @access public
+ * @return IP.
+ */
+function jetpack_protect_get_ip() {
+ $trusted_header_data = get_site_option( 'trusted_ip_header' );
+ if ( isset( $trusted_header_data->trusted_header ) && isset( $_SERVER[ $trusted_header_data->trusted_header ] ) ) {
+ $ip = $_SERVER[ $trusted_header_data->trusted_header ];
+ $segments = $trusted_header_data->segments;
+ $reverse_order = $trusted_header_data->reverse;
+ } else {
+ $ip = $_SERVER['REMOTE_ADDR'];
+ }
+
+ if ( ! $ip ) {
+ return false;
+ }
+
+
+
+ $ips = explode( ',', $ip );
+ if ( ! isset( $segments ) || ! $segments ) {
+ $segments = 1;
+ }
+ if ( isset( $reverse_order ) && $reverse_order ) {
+ $ips = array_reverse( $ips );
+ }
+ $ip_count = count( $ips );
+ if ( 1 === $ip_count ) {
+ return jetpack_clean_ip( $ips[0] );
+ } elseif ( $ip_count >= $segments ) {
+ $the_one = $ip_count - $segments;
+ return jetpack_clean_ip( $ips[ $the_one ] );
+ } else {
+ return jetpack_clean_ip( $_SERVER['REMOTE_ADDR'] );
+ }
+}
+
+/**
+ * Jetpack Clean IP.
+ *
+ * @access public
+ * @param mixed $ip IP.
+ * @return $ip IP.
+ */
+function jetpack_clean_ip( $ip ) {
+
+ // Some misconfigured servers give back extra info, which comes after "unless"
+ $ips = explode( ' unless ', $ip );
+ $ip = $ips[0];
+
+ $ip = trim( $ip );
+ // Check for IPv4 IP cast as IPv6.
+ if ( preg_match( '/^::ffff:(\d+\.\d+\.\d+\.\d+)$/', $ip, $matches ) ) {
+ $ip = $matches[1];
+ }
+
+ if ( function_exists( 'parse_url' ) ) {
+ $parsed_url = parse_url( $ip );
+
+ if ( isset( $parsed_url['host'] ) ) {
+ $ip = $parsed_url['host'];
+ } elseif ( isset( $parsed_url['path'] ) ) {
+ $ip = $parsed_url['path'];
+ }
+ } else {
+ $colon_count = substr_count( $ip, ':' );
+ if ( 1 == $colon_count ) {
+ $ips = explode( ':', $ip );
+ $ip = $ips[0];
+ }
+ }
+
+ return $ip;
+}
+
+/**
+ * Checks an IP to see if it is within a private range.
+ *
+ * @param int $ip IP.
+ * @return bool
+ */
+function jetpack_protect_ip_is_private( $ip ) {
+ // We are dealing with ipv6, so we can simply rely on filter_var.
+ if ( false === strpos( $ip, '.' ) ) {
+ return ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
+ }
+ // We are dealing with ipv4.
+ $private_ip4_addresses = array(
+ '10.0.0.0|10.255.255.255', // Single class A network.
+ '172.16.0.0|172.31.255.255', // 16 contiguous class B network.
+ '192.168.0.0|192.168.255.255', // 256 contiguous class C network.
+ '169.254.0.0|169.254.255.255', // Link-local address also referred to as Automatic Private IP Addressing.
+ '127.0.0.0|127.255.255.255', // localhost.
+ );
+ $long_ip = ip2long( $ip );
+ if ( -1 !== $long_ip ) {
+ foreach ( $private_ip4_addresses as $pri_addr ) {
+ list ( $start, $end ) = explode( '|', $pri_addr );
+ if ( $long_ip >= ip2long( $start ) && $long_ip <= ip2long( $end ) ) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Uses inet_pton if available to convert an IP address to a binary string.
+ * If inet_pton is not available, ip2long will convert the address to an integer.
+ * Returns false if an invalid IP address is given.
+ *
+ * NOTE: ip2long will return false for any ipv6 address. servers that do not support
+ * inet_pton will not support ipv6
+ *
+ * @access public
+ * @param mixed $ip IP.
+ * @return int|string|bool
+ */
+function jetpack_convert_ip_address( $ip ) {
+ if ( function_exists( 'inet_pton' ) ) {
+ return inet_pton( $ip );
+ }
+ return ip2long( $ip );
+}
+
+/**
+ * Checks that a given IP address is within a given low - high range.
+ * Servers that support inet_pton will use that function to convert the ip to number,
+ * while other servers will use ip2long.
+ *
+ * NOTE: servers that do not support inet_pton cannot support ipv6.
+ *
+ * @access public
+ * @param mixed $ip IP.
+ * @param mixed $range_low Range Low.
+ * @param mixed $range_high Range High.
+ * @return Bool.
+ */
+function jetpack_protect_ip_address_is_in_range( $ip, $range_low, $range_high ) {
+ // The inet_pton will give us binary string of an ipv4 or ipv6.
+ // We can then use strcmp to see if the address is in range.
+ if ( function_exists( 'inet_pton' ) ) {
+ $ip_num = inet_pton( $ip );
+ $ip_low = inet_pton( $range_low );
+ $ip_high = inet_pton( $range_high );
+ if ( $ip_num && $ip_low && $ip_high && strcmp( $ip_num, $ip_low ) >= 0 && strcmp( $ip_num, $ip_high ) <= 0 ) {
+ return true;
+ }
+ // The ip2long will give us an integer of an ipv4 address only. it will produce FALSE for ipv6.
+ } else {
+ $ip_num = ip2long( $ip );
+ $ip_low = ip2long( $range_low );
+ $ip_high = ip2long( $range_high );
+ if ( $ip_num && $ip_low && $ip_high && $ip_num >= $ip_low && $ip_num <= $ip_high ) {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/plugins/jetpack/modules/protect/transient-cleanup.php b/plugins/jetpack/modules/protect/transient-cleanup.php
new file mode 100644
index 00000000..8f1c169d
--- /dev/null
+++ b/plugins/jetpack/modules/protect/transient-cleanup.php
@@ -0,0 +1,58 @@
+<?php
+/*
+Adapted from Purge Transients by Seebz
+https://github.com/Seebz/Snippets/tree/master/Wordpress/plugins/purge-transients
+*/
+if ( ! function_exists( 'jp_purge_transients' ) ) {
+
+ /**
+ * Jetpack Purge Transients.
+ *
+ * @access public
+ * @param string $older_than (default: '1 hour') Older Than.
+ * @return void
+ */
+ function jp_purge_transients( $older_than = '1 hour' ) {
+ global $wpdb;
+ $older_than_time = strtotime( '-' . $older_than );
+ if ( $older_than_time > time() || $older_than_time < 1 ) {
+ return false;
+ }
+ $sql = $wpdb->prepare( "
+ SELECT REPLACE(option_name, '_transient_timeout_jpp_', '') AS transient_name
+ FROM {$wpdb->options}
+ WHERE option_name LIKE '\_transient\_timeout\_jpp\__%%'
+ AND option_value < %d
+ ", $older_than_time );
+ $transients = $wpdb->get_col( $sql );
+ $options_names = array();
+ foreach ( $transients as $transient ) {
+ $options_names[] = '_transient_jpp_' . $transient;
+ $options_names[] = '_transient_timeout_jpp_' . $transient;
+ }
+ if ( $options_names ) {
+ $option_names_string = implode( ', ', array_fill( 0, count( $options_names ), '%s' ) );
+ $delete_sql = "DELETE FROM {$wpdb->options} WHERE option_name IN ($option_names_string)";
+ $delete_sql = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $delete_sql ), $options_names ) );
+ $result = $wpdb->query( $delete_sql );
+ if ( ! $result ) {
+ return false;
+ }
+ }
+ return;
+ }
+}
+
+/**
+ * Jetpack Purge Transients Activation.
+ *
+ * @access public
+ * @return void
+ */
+function jp_purge_transients_activation() {
+ if ( ! wp_next_scheduled( 'jp_purge_transients_cron' ) ) {
+ wp_schedule_event( time(), 'daily', 'jp_purge_transients_cron' );
+ }
+}
+add_action( 'admin_init', 'jp_purge_transients_activation' );
+add_action( 'jp_purge_transients_cron', 'jp_purge_transients' );
diff --git a/plugins/jetpack/modules/publicize.php b/plugins/jetpack/modules/publicize.php
new file mode 100644
index 00000000..f5738da3
--- /dev/null
+++ b/plugins/jetpack/modules/publicize.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Module Name: Publicize
+ * Module Description: Publicize makes it easy to share your site’s posts on several social media networks automatically when you publish a new post.
+ * Sort Order: 10
+ * Recommendation Order: 7
+ * First Introduced: 2.0
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Social, Recommended
+ * Feature: Engagement
+ * Additional Search Queries: facebook, jetpack publicize, twitter, tumblr, linkedin, social, tweet, connections, sharing, social media, automated, automated sharing, auto publish, auto tweet and like, auto tweet, facebook auto post, facebook posting
+ */
+
+class Jetpack_Publicize {
+
+ public $in_jetpack = true;
+
+ function __construct() {
+ global $publicize_ui;
+
+ $this->in_jetpack = ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'enable_module_configurable' ) ) ? true : false;
+
+ if ( $this->in_jetpack ) {
+ Jetpack::enable_module_configurable( __FILE__ );
+ }
+
+ require_once dirname( __FILE__ ) . '/publicize/publicize.php';
+
+ if ( $this->in_jetpack )
+ require_once dirname( __FILE__ ) . '/publicize/publicize-jetpack.php';
+ else {
+ require_once dirname( dirname( __FILE__ ) ) . '/mu-plugins/keyring/keyring.php';
+ require_once dirname( __FILE__ ) . '/publicize/publicize-wpcom.php';
+ }
+
+ require_once dirname( __FILE__ ) . '/publicize/ui.php';
+ $publicize_ui = new Publicize_UI();
+ $publicize_ui->in_jetpack = $this->in_jetpack;
+
+ // Jetpack specific checks / hooks
+ if ( $this->in_jetpack ) {
+ // if sharedaddy isn't active, the sharing menu hasn't been added yet
+ $active = Jetpack::get_active_modules();
+ if ( in_array( 'publicize', $active ) && ! in_array( 'sharedaddy', $active ) ) {
+ add_action( 'admin_menu', array( &$publicize_ui, 'sharing_menu' ) );
+ }
+ }
+ }
+}
+
+global $publicize_ui;
+new Jetpack_Publicize;
+
+if( ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) && ! function_exists( 'publicize_init' ) ) {
+/**
+ * Helper for grabbing a Publicize object from the "front-end" (non-admin) of
+ * a site. Normally Publicize is only loaded in wp-admin, so there's a little
+ * set up that you might need to do if you want to use it on the front end.
+ * Just call this function and it returns a Publicize object.
+ *
+ * @return Publicize Object
+ */
+function publicize_init() {
+ global $publicize;
+
+ if ( ! class_exists( 'Publicize' ) ) {
+ require_once dirname( __FILE__ ) . '/publicize/publicize.php';
+ }
+
+ return $publicize;
+}
+
+}
diff --git a/plugins/jetpack/modules/publicize/enhanced-open-graph.php b/plugins/jetpack/modules/publicize/enhanced-open-graph.php
new file mode 100644
index 00000000..ba60b821
--- /dev/null
+++ b/plugins/jetpack/modules/publicize/enhanced-open-graph.php
@@ -0,0 +1,129 @@
+<?php
+if ( ! class_exists( 'Jetpack_Media_Summary' ) ) {
+ if ( defined('IS_WPCOM') && IS_WPCOM ) {
+ include WP_CONTENT_DIR . '/lib/class.wpcom-media-summary.php';
+ } else {
+ jetpack_require_lib( 'class.media-summary' );
+ }
+}
+
+/**
+ * Better OG Image Tags for Image Post Formats
+ */
+function enhanced_og_image( $tags ) {
+ if ( !is_singular() || post_password_required() )
+ return $tags;
+
+ global $post;
+
+ // Always favor featured images.
+ if ( enhanced_og_has_featured_image( $post->ID ) )
+ return $tags;
+
+ $summary = Jetpack_Media_Summary::get( $post->ID );
+
+ if ( 'image' != $summary['type'] )
+ return $tags;
+
+ $tags['og:image'] = $summary['image'];
+ $tags['og:image:secure_url'] = $summary['secure']['image'];
+
+ return $tags;
+}
+add_filter( 'jetpack_open_graph_tags', 'enhanced_og_image' );
+
+/**
+ * Better OG Image Tags for Gallery Post Formats
+ */
+function enhanced_og_gallery( $tags ) {
+ if ( !is_singular() || post_password_required() )
+ return $tags;
+
+ global $post;
+
+ // Always favor featured images.
+ if ( enhanced_og_has_featured_image( $post->ID ) )
+ return $tags;
+
+ $summary = Jetpack_Media_Summary::get( $post->ID );
+
+ if ( 'gallery' != $summary['type'] )
+ return $tags;
+
+ if( !isset( $summary['images'] ) || !is_array( $summary['images'] ) || empty( $summary['images'] ) )
+ return $tags;
+
+ $images = $secures = array();
+ foreach ( $summary['images'] as $i => $image ) {
+ $images[] = $image['url'];
+ $secures[] = $summary['secure']['images'][$i]['url'];
+ }
+
+ $tags['og:image'] = $images;
+ $tags['og:image:secure_url'] = $secures;
+
+ return $tags;
+}
+add_filter( 'jetpack_open_graph_tags', 'enhanced_og_gallery' );
+
+/**
+ * Allows VideoPress, YouTube, and Vimeo videos to play inline on Facebook
+ */
+function enhanced_og_video( $tags ) {
+ if ( !is_singular() || post_password_required() )
+ return $tags;
+
+ global $post;
+
+ // Always favor featured images.
+ if ( enhanced_og_has_featured_image( $post->ID ) )
+ return $tags;
+
+ $summary = Jetpack_Media_Summary::get( $post->ID );
+
+ if ( 'video' != $summary['type'] ) {
+ if ( $summary['count']['video'] > 0 && $summary['count']['image'] < 1 ) {
+ $tags['og:image'] = $summary['image'];
+ $tags['og:image:secure_url'] = $summary['secure']['image'];
+ }
+ return $tags;
+ }
+
+ $tags['og:image'] = $summary['image'];
+ $tags['og:image:secure_url'] = $summary['secure']['image'];
+
+ // This should be html by default for youtube/vimeo, since we're linking to HTML pages.
+ $tags['og:video:type'] = isset( $summary['video_type'] ) ? $summary['video_type'] : 'text/html';
+
+ $video_url = $summary['video'];
+ $secure_video_url = $summary['secure']['video'];
+
+ if ( preg_match( '/((youtube|vimeo)\.com|youtu.be)/', $video_url ) ) {
+ if ( strstr( $video_url, 'youtube' ) ) {
+ $id = jetpack_get_youtube_id( $video_url );
+ $video_url = 'http://www.youtube.com/embed/' . $id;
+ $secure_video_url = 'https://www.youtube.com/embed/' . $id;
+ } else if ( strstr( $video_url, 'vimeo' ) ) {
+ preg_match( '|vimeo\.com/(\d+)/?$|i', $video_url, $match );
+ $id = (int) $match[1];
+ $video_url = 'http://vimeo.com/moogaloop.swf?clip_id=' . $id;
+ $secure_video_url = 'https://vimeo.com/moogaloop.swf?clip_id=' . $id;
+ }
+ }
+
+ $tags['og:video'] = $video_url;
+ $tags['og:video:secure_url'] = $secure_video_url;
+
+ if ( empty( $post->post_title ) )
+ $tags['og:title'] = sprintf( __( 'Video on %s', 'jetpack' ), get_option( 'blogname' ) );
+
+ return $tags;
+}
+add_filter( 'jetpack_open_graph_tags', 'enhanced_og_video' );
+
+function enhanced_og_has_featured_image( $post_id ) {
+ $featured = Jetpack_PostImages::from_thumbnail( $post_id, 200, 200 );
+ if ( !empty( $featured ) && count( $featured ) > 0 )
+ return true;
+ return false;
+}
diff --git a/plugins/jetpack/modules/publicize/publicize-jetpack.php b/plugins/jetpack/modules/publicize/publicize-jetpack.php
new file mode 100644
index 00000000..b2ec20e8
--- /dev/null
+++ b/plugins/jetpack/modules/publicize/publicize-jetpack.php
@@ -0,0 +1,753 @@
+<?php
+
+class Publicize extends Publicize_Base {
+
+ function __construct() {
+ parent::__construct();
+
+ add_filter( 'jetpack_xmlrpc_methods', array( $this, 'register_update_publicize_connections_xmlrpc_method' ) );
+
+ add_action( 'load-settings_page_sharing', array( $this, 'admin_page_load' ), 9 );
+
+ add_action( 'wp_ajax_publicize_tumblr_options_page', array( $this, 'options_page_tumblr' ) );
+ add_action( 'wp_ajax_publicize_facebook_options_page', array( $this, 'options_page_facebook' ) );
+ add_action( 'wp_ajax_publicize_twitter_options_page', array( $this, 'options_page_twitter' ) );
+ add_action( 'wp_ajax_publicize_linkedin_options_page', array( $this, 'options_page_linkedin' ) );
+
+ add_action( 'wp_ajax_publicize_tumblr_options_save', array( $this, 'options_save_tumblr' ) );
+ add_action( 'wp_ajax_publicize_facebook_options_save', array( $this, 'options_save_facebook' ) );
+ add_action( 'wp_ajax_publicize_twitter_options_save', array( $this, 'options_save_twitter' ) );
+ add_action( 'wp_ajax_publicize_linkedin_options_save', array( $this, 'options_save_linkedin' ) );
+
+ add_action( 'load-settings_page_sharing', array( $this, 'force_user_connection' ) );
+
+ add_filter( 'jetpack_published_post_flags', array( $this, 'set_post_flags' ), 10, 2 );
+
+ add_action( 'wp_insert_post', array( $this, 'save_publicized' ), 11, 3 );
+
+ add_filter( 'jetpack_twitter_cards_site_tag', array( $this, 'enhaced_twitter_cards_site_tag' ) );
+
+ add_action( 'publicize_save_meta', array( $this, 'save_publicized_twitter_account' ), 10, 4 );
+ add_action( 'publicize_save_meta', array( $this, 'save_publicized_facebook_account' ), 10, 4 );
+
+ add_action( 'connection_disconnected', array( $this, 'add_disconnect_notice' ) );
+
+ add_filter( 'jetpack_sharing_twitter_via', array( $this, 'get_publicized_twitter_account' ), 10, 2 );
+
+ include_once( JETPACK__PLUGIN_DIR . 'modules/publicize/enhanced-open-graph.php' );
+
+ jetpack_require_lib( 'class.jetpack-keyring-service-helper' );
+ }
+
+ function add_disconnect_notice() {
+ add_action( 'admin_notices', array( $this, 'display_disconnected' ) );
+ }
+
+ function force_user_connection() {
+ global $current_user;
+ $user_token = Jetpack_Data::get_access_token( $current_user->ID );
+ $is_user_connected = $user_token && ! is_wp_error( $user_token );
+
+ // If the user is already connected via Jetpack, then we're good
+ if ( $is_user_connected ) {
+ return;
+ }
+
+ // If they're not connected, then remove the Publicize UI and tell them they need to connect first
+ global $publicize_ui;
+ remove_action( 'pre_admin_screen_sharing', array( $publicize_ui, 'admin_page' ) );
+
+ // Do we really need `admin_styles`? With the new admin UI, it's breaking some bits.
+ // Jetpack::init()->admin_styles();
+ add_action( 'pre_admin_screen_sharing', array( $this, 'admin_page_warning' ), 1 );
+ }
+
+ function admin_page_warning() {
+ $jetpack = Jetpack::init();
+ $blog_name = get_bloginfo( 'blogname' );
+ if ( empty( $blog_name ) ) {
+ $blog_name = home_url( '/' );
+ }
+
+ ?>
+ <div id="message" class="updated jetpack-message jp-connect">
+ <div class="jetpack-wrap-container">
+ <div class="jetpack-text-container">
+ <p><?php printf(
+ /* translators: %s is the name of the blog */
+ esc_html( wptexturize( __( "To use Publicize, you'll need to link your %s account to your WordPress.com account using the link below.", 'jetpack' ) ) ),
+ '<strong>' . esc_html( $blog_name ) . '</strong>'
+ ); ?></p>
+ <p><?php echo esc_html( wptexturize( __( "If you don't have a WordPress.com account yet, you can sign up for free in just a few seconds.", 'jetpack' ) ) ); ?></p>
+ </div>
+ <div class="jetpack-install-container">
+ <p class="submit"><a
+ href="<?php echo $jetpack->build_connect_url( false, menu_page_url( 'sharing', false ) ); ?>"
+ class="button-connector"
+ id="wpcom-connect"><?php esc_html_e( 'Link account with WordPress.com', 'jetpack' ); ?></a>
+ </p>
+ <p class="jetpack-install-blurb">
+ <?php jetpack_render_tos_blurb(); ?>
+ </p>
+ </div>
+ </div>
+ </div>
+ <?php
+ }
+
+ /**
+ * Remove a Publicize connection
+ */
+ function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false ) {
+ return Jetpack_Keyring_Service_Helper::disconnect( $service_name, $connection_id, $_blog_id, $_user_id, $force_delete );
+ }
+
+ function receive_updated_publicize_connections( $publicize_connections ) {
+ Jetpack_Options::update_option( 'publicize_connections', $publicize_connections );
+
+ return true;
+ }
+
+ function register_update_publicize_connections_xmlrpc_method( $methods ) {
+ return array_merge( $methods, array(
+ 'jetpack.updatePublicizeConnections' => array( $this, 'receive_updated_publicize_connections' ),
+ ) );
+ }
+
+ function get_all_connections() {
+ $connections = Jetpack_Options::get_option( 'publicize_connections' );
+ if ( isset( $connections['google_plus'] ) ) {
+ unset( $connections['google_plus'] );
+ }
+ return $connections;
+ }
+
+ function get_connections( $service_name, $_blog_id = false, $_user_id = false ) {
+ if ( false === $_user_id ) {
+ $_user_id = $this->user_id();
+ }
+
+ $connections = $this->get_all_connections();
+ $connections_to_return = array();
+
+ if ( ! empty( $connections ) && is_array( $connections ) ) {
+ if ( ! empty( $connections[ $service_name ] ) ) {
+ foreach ( $connections[ $service_name ] as $id => $connection ) {
+ if ( 0 == $connection['connection_data']['user_id'] || $_user_id == $connection['connection_data']['user_id'] ) {
+ $connections_to_return[ $id ] = $connection;
+ }
+ }
+ }
+
+ return $connections_to_return;
+ }
+
+ return false;
+ }
+
+ function get_all_connections_for_user() {
+ $connections = $this->get_all_connections();
+
+ $connections_to_return = array();
+ if ( ! empty( $connections ) ) {
+ foreach ( (array) $connections as $service_name => $connections_for_service ) {
+ foreach ( $connections_for_service as $id => $connection ) {
+ $user_id = intval( $connection['connection_data']['user_id'] );
+ // phpcs:ignore WordPress.PHP.YodaConditions.NotYoda
+ if ( $user_id === 0 || $this->user_id() === $user_id ) {
+ $connections_to_return[ $service_name ][ $id ] = $connection;
+ }
+ }
+ }
+
+ return $connections_to_return;
+ }
+
+ return false;
+ }
+
+ function get_connection_id( $connection ) {
+ return $connection['connection_data']['id'];
+ }
+
+ function get_connection_unique_id( $connection ) {
+ return $connection['connection_data']['token_id'];
+ }
+
+ function get_connection_meta( $connection ) {
+ $connection['user_id'] = $connection['connection_data']['user_id']; // Allows for shared connections
+ return $connection;
+ }
+
+ function admin_page_load() {
+ if ( isset( $_GET['action'] ) && 'error' === $_GET['action'] ) {
+ add_action( 'pre_admin_screen_sharing', array( $this, 'display_connection_error' ), 9 );
+ }
+ }
+
+ function display_connection_error() {
+ $code = false;
+ if ( isset( $_GET['service'] ) ) {
+ $service_name = $_GET['service'];
+ $error = sprintf( __( 'There was a problem connecting to %s to create an authorized connection. Please try again in a moment.', 'jetpack' ), Publicize::get_service_label( $service_name ) );
+ } else {
+ if ( isset( $_GET['publicize_error'] ) ) {
+ $code = strtolower( $_GET['publicize_error'] );
+ switch ( $code ) {
+ case '400':
+ $error = __( 'An invalid request was made. This normally means that something intercepted or corrupted the request from your server to the Jetpack Server. Try again and see if it works this time.', 'jetpack' );
+ break;
+ case 'secret_mismatch':
+ $error = __( 'We could not verify that your server is making an authorized request. Please try again, and make sure there is nothing interfering with requests from your server to the Jetpack Server.', 'jetpack' );
+ break;
+ case 'empty_blog_id':
+ $error = __( 'No blog_id was included in your request. Please try disconnecting Jetpack from WordPress.com and then reconnecting it. Once you have done that, try connecting Publicize again.', 'jetpack' );
+ break;
+ case 'empty_state':
+ $error = sprintf( __( 'No user information was included in your request. Please make sure that your user account has connected to Jetpack. Connect your user account by going to the <a href="%s">Jetpack page</a> within wp-admin.', 'jetpack' ), Jetpack::admin_url() );
+ break;
+ default:
+ $error = __( 'Something which should never happen, happened. Sorry about that. If you try again, maybe it will work.', 'jetpack' );
+ break;
+ }
+ } else {
+ $error = __( 'There was a problem connecting with Publicize. Please try again in a moment.', 'jetpack' );
+ }
+ }
+ // Using the same formatting/style as Jetpack::admin_notices() error
+ ?>
+ <div id="message" class="jetpack-message jetpack-err">
+ <div class="squeezer">
+ <h2><?php echo wp_kses( $error, array( 'a' => array( 'href' => true ),
+ 'code' => true,
+ 'strong' => true,
+ 'br' => true,
+ 'b' => true
+ ) ); ?></h2>
+ <?php if ( $code ) : ?>
+ <p><?php printf( __( 'Error code: %s', 'jetpack' ), esc_html( stripslashes( $code ) ) ); ?></p>
+ <?php endif; ?>
+ </div>
+ </div>
+ <?php
+ }
+
+ function display_disconnected() {
+ echo "<div class='updated'>\n";
+ echo '<p>' . esc_html( __( 'That connection has been removed.', 'jetpack' ) ) . "</p>\n";
+ echo "</div>\n\n";
+ }
+
+ function globalization() {
+ if ( 'on' == $_REQUEST['global'] ) {
+ $globalize_connection = $_REQUEST['connection'];
+ if ( ! current_user_can( $this->GLOBAL_CAP ) ) {
+ return;
+ }
+
+ $this->globalize_connection( $globalize_connection );
+ }
+ }
+
+ function globalize_connection( $connection_id ) {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client();
+ $xml->query( 'jetpack.globalizePublicizeConnection', $connection_id, 'globalize' );
+
+ if ( ! $xml->isError() ) {
+ $response = $xml->getResponse();
+ $this->receive_updated_publicize_connections( $response );
+ }
+ }
+
+ function unglobalize_connection( $connection_id ) {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client();
+ $xml->query( 'jetpack.globalizePublicizeConnection', $connection_id, 'unglobalize' );
+
+ if ( ! $xml->isError() ) {
+ $response = $xml->getResponse();
+ $this->receive_updated_publicize_connections( $response );
+ }
+ }
+
+ function connect_url( $service_name, $for = 'publicize' ) {
+ return Jetpack_Keyring_Service_Helper::connect_url( $service_name, $for );
+ }
+
+ function refresh_url( $service_name, $for = 'publicize' ) {
+ return Jetpack_Keyring_Service_Helper::refresh_url( $service_name, $for );
+ }
+
+ function disconnect_url( $service_name, $id ) {
+ return Jetpack_Keyring_Service_Helper::disconnect_url( $service_name, $id );
+ }
+
+ /**
+ * Get social networks, either all available or only those that the site is connected to.
+ *
+ * @since 2.0.0
+ * @since 6.6.0 Removed Path. Service closed October 2018.
+ *
+ * @param string $filter Select the list of services that will be returned. Defaults to 'all', accepts 'connected'.
+ *
+ * @return array List of social networks.
+ */
+ function get_services( $filter = 'all', $_blog_id = false, $_user_id = false ) {
+ $services = array(
+ 'facebook' => array(),
+ 'twitter' => array(),
+ 'linkedin' => array(),
+ 'tumblr' => array(),
+ );
+
+ if ( 'all' == $filter ) {
+ return $services;
+ } else {
+ $connected_services = array();
+ foreach ( $services as $service_name => $empty ) {
+ $connections = $this->get_connections( $service_name, $_blog_id, $_user_id );
+ if ( $connections ) {
+ $connected_services[ $service_name ] = $connections;
+ }
+ }
+ return $connected_services;
+ }
+ }
+
+ function get_connection( $service_name, $id, $_blog_id = false, $_user_id = false ) {
+ // Stub
+ }
+
+ function flag_post_for_publicize( $new_status, $old_status, $post ) {
+ if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) {
+ return;
+ }
+
+ if ( 'publish' == $new_status && 'publish' != $old_status ) {
+ /**
+ * Determines whether a post being published gets publicized.
+ *
+ * Side-note: Possibly our most alliterative filter name.
+ *
+ * @module publicize
+ *
+ * @since 4.1.0
+ *
+ * @param bool $should_publicize Should the post be publicized? Default to true.
+ * @param WP_POST $post Current Post object.
+ */
+ $should_publicize = apply_filters( 'publicize_should_publicize_published_post', true, $post );
+
+ if ( $should_publicize ) {
+ update_post_meta( $post->ID, $this->PENDING, true );
+ }
+ }
+ }
+
+ function test_connection( $service_name, $connection ) {
+
+ $id = $this->get_connection_id( $connection );
+
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client();
+ $xml->query( 'jetpack.testPublicizeConnection', $id );
+
+ // Bail if all is well
+ if ( ! $xml->isError() ) {
+ return true;
+ }
+
+ $xml_response = $xml->getResponse();
+ $connection_test_message = $xml_response['faultString'];
+
+ // Set up refresh if the user can
+ $user_can_refresh = current_user_can( $this->GLOBAL_CAP );
+ if ( $user_can_refresh ) {
+ $nonce = wp_create_nonce( "keyring-request-" . $service_name );
+ $refresh_text = sprintf( _x( 'Refresh connection with %s', 'Refresh connection with {social media service}', 'jetpack' ), $this->get_service_label( $service_name ) );
+ $refresh_url = $this->refresh_url( $service_name );
+ }
+
+ $error_data = array(
+ 'user_can_refresh' => $user_can_refresh,
+ 'refresh_text' => $refresh_text,
+ 'refresh_url' => $refresh_url
+ );
+
+ return new WP_Error( 'pub_conn_test_failed', $connection_test_message, $error_data );
+ }
+
+ /**
+ * Checks if post has already been shared by Publicize in the past.
+ *
+ * Jetpack uses two methods:
+ * 1. A POST_DONE . 'all' postmeta flag, or
+ * 2. if the post has already been published.
+ *
+ * @since 6.7.0
+ *
+ * @param integer $post_id Optional. Post ID to query connection status for: will use current post if missing.
+ *
+ * @return bool True if post has already been shared by Publicize, false otherwise.
+ */
+ public function post_is_done_sharing( $post_id = null ) {
+ // Defaults to current post if $post_id is null.
+ $post = get_post( $post_id );
+ if ( is_null( $post ) ) {
+ return false;
+ }
+
+ return 'publish' == $post->post_status || get_post_meta( $post->ID, $this->POST_DONE . 'all', true );
+ }
+
+ /**
+ * Save a flag locally to indicate that this post has already been Publicized via the selected
+ * connections.
+ */
+ function save_publicized( $post_ID, $post = null, $update = null ) {
+ if ( is_null( $post ) ) {
+ return;
+ }
+ // Only do this when a post transitions to being published
+ if ( get_post_meta( $post->ID, $this->PENDING ) && $this->post_type_is_publicizeable( $post->post_type ) ) {
+ $connected_services = $this->get_all_connections();
+ if ( ! empty( $connected_services ) ) {
+ /**
+ * Fires when a post is saved that has is marked as pending publicizing
+ *
+ * @since 4.1.0
+ *
+ * @param int The post ID
+ */
+ do_action_deprecated( 'jetpack_publicize_post', $post->ID, '4.8.0', 'jetpack_published_post_flags' );
+ }
+ delete_post_meta( $post->ID, $this->PENDING );
+ update_post_meta( $post->ID, $this->POST_DONE . 'all', true );
+ }
+ }
+
+ function set_post_flags( $flags, $post ) {
+ $flags['publicize_post'] = false;
+ if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) {
+ return $flags;
+ }
+ /** This filter is already documented in modules/publicize/publicize-jetpack.php */
+ if ( ! apply_filters( 'publicize_should_publicize_published_post', true, $post ) ) {
+ return $flags;
+ }
+
+ $connected_services = $this->get_all_connections();
+
+ if ( empty( $connected_services ) ) {
+ return $flags;
+ }
+
+ $flags['publicize_post'] = true;
+
+ return $flags;
+ }
+
+ /**
+ * Options Code
+ */
+
+ function options_page_facebook() {
+ $connected_services = $this->get_all_connections();
+ $connection = $connected_services['facebook'][ $_REQUEST['connection'] ];
+ $options_to_show = ( ! empty( $connection['connection_data']['meta']['options_responses'] ) ? $connection['connection_data']['meta']['options_responses'] : false );
+
+ // Nonce check
+ check_admin_referer( 'options_page_facebook_' . $_REQUEST['connection'] );
+
+ $pages = ( ! empty( $options_to_show[1]['data'] ) ? $options_to_show[1]['data'] : false );
+
+ $page_selected = false;
+ if ( ! empty( $connection['connection_data']['meta']['facebook_page'] ) ) {
+ $found = false;
+ if ( $pages && isset( $pages->data ) && is_array( $pages->data ) ) {
+ foreach ( $pages->data as $page ) {
+ if ( $page->id == $connection['connection_data']['meta']['facebook_page'] ) {
+ $found = true;
+ break;
+ }
+ }
+ }
+
+ if ( $found ) {
+ $page_selected = $connection['connection_data']['meta']['facebook_page'];
+ }
+ }
+
+ ?>
+
+ <div id="thickbox-content">
+
+ <?php
+ ob_start();
+ Publicize_UI::connected_notice( 'Facebook' );
+ $update_notice = ob_get_clean();
+
+ if ( ! empty( $update_notice ) ) {
+ echo $update_notice;
+ }
+ $page_info_message = sprintf(
+ __( 'Facebook supports Publicize connections to Facebook Pages, but not to Facebook Profiles. <a href="%s">Learn More about Publicize for Facebook</a>', 'jetpack' ),
+ 'https://jetpack.com/support/publicize/facebook'
+ );
+
+ if ( $pages ) : ?>
+ <p><?php _e( 'Publicize to my <strong>Facebook Page</strong>:', 'jetpack' ); ?></p>
+ <table id="option-fb-fanpage">
+ <tbody>
+
+ <?php foreach ( $pages as $i => $page ) : ?>
+ <?php if ( ! ( $i % 2 ) ) : ?>
+ <tr>
+ <?php endif; ?>
+ <td class="radio"><input type="radio" name="option" data-type="page"
+ id="<?php echo esc_attr( $page['id'] ) ?>"
+ value="<?php echo esc_attr( $page['id'] ) ?>" <?php checked( $page_selected && $page_selected == $page['id'], true ); ?> />
+ </td>
+ <td class="thumbnail"><label for="<?php echo esc_attr( $page['id'] ) ?>"><img
+ src="<?php echo esc_url( str_replace( '_s', '_q', $page['picture']['data']['url'] ) ) ?>"
+ width="50" height="50"/></label></td>
+ <td class="details">
+ <label for="<?php echo esc_attr( $page['id'] ) ?>">
+ <span class="name"><?php echo esc_html( $page['name'] ) ?></span><br/>
+ <span class="category"><?php echo esc_html( $page['category'] ) ?></span>
+ </label>
+ </td>
+ <?php if ( ( $i % 2 ) || ( $i == count( $pages ) - 1 ) ): ?>
+ </tr>
+ <?php endif; ?>
+ <?php endforeach; ?>
+
+ </tbody>
+ </table>
+
+ <?php Publicize_UI::global_checkbox( 'facebook', $_REQUEST['connection'] ); ?>
+ <p style="text-align: center;">
+ <input type="submit" value="<?php esc_attr_e( 'OK', 'jetpack' ) ?>"
+ class="button fb-options save-options" name="save"
+ data-connection="<?php echo esc_attr( $_REQUEST['connection'] ); ?>"
+ rel="<?php echo wp_create_nonce( 'save_fb_token_' . $_REQUEST['connection'] ) ?>"/>
+ </p><br/>
+ <p><?php echo $page_info_message; ?></p>
+ <?php else: ?>
+ <div>
+ <p><?php echo $page_info_message; ?></p>
+ <p><?php printf( __( '<a class="button" href="%s" target="%s">Create a Facebook page</a> to get started.', 'jetpack' ), 'https://www.facebook.com/pages/creation/', '_blank noopener noreferrer' ); ?></p>
+ </div>
+ <?php endif; ?>
+ </div>
+ <?php
+ }
+
+ function options_save_facebook() {
+ // Nonce check
+ check_admin_referer( 'save_fb_token_' . $_REQUEST['connection'] );
+
+ // Check for a numeric page ID
+ $page_id = $_POST['selected_id'];
+ if ( ! ctype_digit( $page_id ) ) {
+ die( 'Security check' );
+ }
+
+ if ( 'page' != $_POST['type'] || ! isset( $_POST['selected_id'] ) ) {
+ return;
+ }
+
+ // Publish to Page
+ $options = array(
+ 'facebook_page' => $page_id,
+ 'facebook_profile' => null
+ );
+
+ $this->set_remote_publicize_options( $_POST['connection'], $options );
+ }
+
+ function options_page_tumblr() {
+ // Nonce check
+ check_admin_referer( 'options_page_tumblr_' . $_REQUEST['connection'] );
+
+ $connected_services = $this->get_all_connections();
+ $connection = $connected_services['tumblr'][ $_POST['connection'] ];
+ $options_to_show = $connection['connection_data']['meta']['options_responses'];
+ $request = $options_to_show[0];
+
+ $blogs = $request['response']['user']['blogs'];
+
+ $blog_selected = false;
+
+ if ( ! empty( $connection['connection_data']['meta']['tumblr_base_hostname'] ) ) {
+ foreach ( $blogs as $blog ) {
+ if ( $connection['connection_data']['meta']['tumblr_base_hostname'] == $this->get_basehostname( $blog['url'] ) ) {
+ $blog_selected = $connection['connection_data']['meta']['tumblr_base_hostname'];
+ break;
+ }
+ }
+
+ }
+
+ // Use their Primary blog if they haven't selected one yet
+ if ( ! $blog_selected ) {
+ foreach ( $blogs as $blog ) {
+ if ( $blog['primary'] ) {
+ $blog_selected = $this->get_basehostname( $blog['url'] );
+ }
+ }
+ } ?>
+
+ <div id="thickbox-content">
+
+ <?php
+ ob_start();
+ Publicize_UI::connected_notice( 'Tumblr' );
+ $update_notice = ob_get_clean();
+
+ if ( ! empty( $update_notice ) ) {
+ echo $update_notice;
+ }
+ ?>
+
+ <p><?php _e( 'Publicize to my <strong>Tumblr blog</strong>:', 'jetpack' ); ?></p>
+
+ <ul id="option-tumblr-blog">
+
+ <?php
+ foreach ( $blogs as $blog ) {
+ $url = $this->get_basehostname( $blog['url'] ); ?>
+ <li>
+ <input type="radio" name="option" data-type="blog" id="<?php echo esc_attr( $url ) ?>"
+ value="<?php echo esc_attr( $url ) ?>" <?php checked( $blog_selected == $url, true ); ?> />
+ <label for="<?php echo esc_attr( $url ) ?>"><span
+ class="name"><?php echo esc_html( $blog['title'] ) ?></span></label>
+ </li>
+ <?php } ?>
+
+ </ul>
+
+ <?php Publicize_UI::global_checkbox( 'tumblr', $_REQUEST['connection'] ); ?>
+
+ <p style="text-align: center;">
+ <input type="submit" value="<?php esc_attr_e( 'OK', 'jetpack' ) ?>"
+ class="button tumblr-options save-options" name="save"
+ data-connection="<?php echo esc_attr( $_REQUEST['connection'] ); ?>"
+ rel="<?php echo wp_create_nonce( 'save_tumblr_blog_' . $_REQUEST['connection'] ) ?>"/>
+ </p> <br/>
+ </div>
+
+ <?php
+ }
+
+ function get_basehostname( $url ) {
+ return parse_url( $url, PHP_URL_HOST );
+ }
+
+ function options_save_tumblr() {
+ // Nonce check
+ check_admin_referer( 'save_tumblr_blog_' . $_REQUEST['connection'] );
+ $options = array( 'tumblr_base_hostname' => $_POST['selected_id'] );
+
+ $this->set_remote_publicize_options( $_POST['connection'], $options );
+
+ }
+
+ function set_remote_publicize_options( $id, $options ) {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client();
+ $xml->query( 'jetpack.setPublicizeOptions', $id, $options );
+
+ if ( ! $xml->isError() ) {
+ $response = $xml->getResponse();
+ Jetpack_Options::update_option( 'publicize_connections', $response );
+ $this->globalization();
+ }
+ }
+
+ function options_page_twitter() {
+ Publicize_UI::options_page_other( 'twitter' );
+ }
+
+ function options_page_linkedin() {
+ Publicize_UI::options_page_other( 'linkedin' );
+ }
+
+ function options_save_twitter() {
+ $this->options_save_other( 'twitter' );
+ }
+
+ function options_save_linkedin() {
+ $this->options_save_other( 'linkedin' );
+ }
+
+ function options_save_other( $service_name ) {
+ // Nonce check
+ check_admin_referer( 'save_' . $service_name . '_token_' . $_REQUEST['connection'] );
+ $this->globalization();
+ }
+
+ /**
+ * If there's only one shared connection to Twitter set it as twitter:site tag.
+ */
+ function enhaced_twitter_cards_site_tag( $tag ) {
+ $custom_site_tag = get_option( 'jetpack-twitter-cards-site-tag' );
+ if ( ! empty( $custom_site_tag ) ) {
+ return $tag;
+ }
+ if ( ! $this->is_enabled( 'twitter' ) ) {
+ return $tag;
+ }
+ $connections = $this->get_connections( 'twitter' );
+ foreach ( $connections as $connection ) {
+ $connection_meta = $this->get_connection_meta( $connection );
+ if ( 0 == $connection_meta['connection_data']['user_id'] ) {
+ // If the connection is shared
+ return $this->get_display_name( 'twitter', $connection );
+ }
+ }
+
+ return $tag;
+ }
+
+ function save_publicized_twitter_account( $submit_post, $post_id, $service_name, $connection ) {
+ if ( 'twitter' == $service_name && $submit_post ) {
+ $connection_meta = $this->get_connection_meta( $connection );
+ $publicize_twitter_user = get_post_meta( $post_id, '_publicize_twitter_user' );
+ if ( empty( $publicize_twitter_user ) || 0 != $connection_meta['connection_data']['user_id'] ) {
+ update_post_meta( $post_id, '_publicize_twitter_user', $this->get_display_name( 'twitter', $connection ) );
+ }
+ }
+ }
+
+ function get_publicized_twitter_account( $account, $post_id ) {
+ if ( ! empty( $account ) ) {
+ return $account;
+ }
+ $account = get_post_meta( $post_id, '_publicize_twitter_user', true );
+ if ( ! empty( $account ) ) {
+ return $account;
+ }
+
+ return '';
+ }
+
+ /**
+ * Save the Publicized Facebook account when publishing a post
+ * Use only Personal accounts, not Facebook Pages
+ */
+ function save_publicized_facebook_account( $submit_post, $post_id, $service_name, $connection ) {
+ $connection_meta = $this->get_connection_meta( $connection );
+ if ( 'facebook' == $service_name && isset( $connection_meta['connection_data']['meta']['facebook_profile'] ) && $submit_post ) {
+ $publicize_facebook_user = get_post_meta( $post_id, '_publicize_facebook_user' );
+ if ( empty( $publicize_facebook_user ) || 0 != $connection_meta['connection_data']['user_id'] ) {
+ $profile_link = $this->get_profile_link( 'facebook', $connection );
+
+ if ( false !== $profile_link ) {
+ update_post_meta( $post_id, '_publicize_facebook_user', $profile_link );
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/jetpack/modules/publicize/publicize.php b/plugins/jetpack/modules/publicize/publicize.php
new file mode 100644
index 00000000..c3972cf6
--- /dev/null
+++ b/plugins/jetpack/modules/publicize/publicize.php
@@ -0,0 +1,1249 @@
+<?php
+
+abstract class Publicize_Base {
+
+ /**
+ * Services that are currently connected to the given user
+ * through publicize.
+ */
+ public $connected_services = array();
+
+ /**
+ * Services that are supported by publicize. They don't
+ * necessarily need to be connected to the current user.
+ */
+ public $services;
+
+ /**
+ * key names for post meta
+ */
+ public $ADMIN_PAGE = 'wpas';
+ public $POST_MESS = '_wpas_mess';
+ public $POST_SKIP = '_wpas_skip_'; // connection id appended to indicate that a connection should NOT be publicized to
+ public $POST_DONE = '_wpas_done_'; // connection id appended to indicate a connection has already been publicized to
+ public $USER_AUTH = 'wpas_authorize';
+ public $USER_OPT = 'wpas_';
+ public $PENDING = '_publicize_pending'; // ready for Publicize to do its thing
+ public $POST_SERVICE_DONE = '_publicize_done_external'; // array of external ids where we've Publicized
+
+ /**
+ * default pieces of the message used in constructing the
+ * content pushed out to other social networks
+ */
+
+ public $default_prefix = '';
+ public $default_message = '%title%';
+ public $default_suffix = ' ';
+
+ /**
+ * What WP capability is require to create/delete global connections?
+ * All users with this cap can un-globalize all other global connections, and globalize any of their own
+ * Globalized connections cannot be unselected by users without this capability when publishing
+ */
+ public $GLOBAL_CAP = 'publish_posts';
+
+ /**
+ * Sets up the basics of Publicize
+ */
+ function __construct() {
+ $this->default_message = self::build_sprintf( array(
+ /**
+ * Filter the default Publicize message.
+ *
+ * @module publicize
+ *
+ * @since 2.0.0
+ *
+ * @param string $this->default_message Publicize's default message. Default is the post title.
+ */
+ apply_filters( 'wpas_default_message', $this->default_message ),
+ 'title',
+ 'url',
+ ) );
+
+ $this->default_prefix = self::build_sprintf( array(
+ /**
+ * Filter the message prepended to the Publicize custom message.
+ *
+ * @module publicize
+ *
+ * @since 2.0.0
+ *
+ * @param string $this->default_prefix String prepended to the Publicize custom message.
+ */
+ apply_filters( 'wpas_default_prefix', $this->default_prefix ),
+ 'url',
+ ) );
+
+ $this->default_suffix = self::build_sprintf( array(
+ /**
+ * Filter the message appended to the Publicize custom message.
+ *
+ * @module publicize
+ *
+ * @since 2.0.0
+ *
+ * @param string $this->default_suffix String appended to the Publicize custom message.
+ */
+ apply_filters( 'wpas_default_suffix', $this->default_suffix ),
+ 'url',
+ ) );
+
+ /**
+ * Filter the capability to change global Publicize connection options.
+ *
+ * All users with this cap can un-globalize all other global connections, and globalize any of their own
+ * Globalized connections cannot be unselected by users without this capability when publishing.
+ *
+ * @module publicize
+ *
+ * @since 2.2.1
+ *
+ * @param string $this->GLOBAL_CAP default capability in control of global Publicize connection options. Default to edit_others_posts.
+ */
+ $this->GLOBAL_CAP = apply_filters( 'jetpack_publicize_global_connections_cap', $this->GLOBAL_CAP );
+
+ // stage 1 and 2 of 3-stage Publicize. Flag for Publicize on creation, save meta,
+ // then check meta and publicize based on that. stage 3 implemented on wpcom
+ add_action( 'transition_post_status', array( $this, 'flag_post_for_publicize' ), 10, 3 );
+ add_action( 'save_post', array( &$this, 'save_meta' ), 20, 2 );
+
+ // Default checkbox state for each Connection
+ add_filter( 'publicize_checkbox_default', array( $this, 'publicize_checkbox_default' ), 10, 4 );
+
+ // Alter the "Post Publish" admin notice to mention the Connections we Publicized to.
+ add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 20, 1 );
+
+ // Connection test callback
+ add_action( 'wp_ajax_test_publicize_conns', array( $this, 'test_publicize_conns' ) );
+
+ add_action( 'init', array( $this, 'add_post_type_support' ) );
+ add_action( 'init', array( $this, 'register_post_meta' ), 20 );
+ add_action( 'jetpack_register_gutenberg_extensions', array( $this, 'register_gutenberg_extension' ) );
+ }
+
+/*
+ * Services: Facebook, Twitter, etc.
+ */
+
+ /**
+ * Get services for the given blog and user.
+ *
+ * Can return all available services or just the ones with an active connection.
+ *
+ * @param string $filter
+ * 'all' (default) - Get all services available for connecting
+ * 'connected' - Get all services currently connected
+ * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
+ * @param false|int $_user_id The user ID. Use false (default) for the current user
+ * @return array
+ */
+ abstract function get_services( $filter = 'all', $_blog_id = false, $_user_id = false );
+
+ function can_connect_service( $service_name ) {
+ return true;
+ }
+
+ /**
+ * Does the given user have a connection to the service on the given blog?
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
+ * @param false|int $_user_id The user ID. Use false (default) for the current user
+ * @return bool
+ */
+ function is_enabled( $service_name, $_blog_id = false, $_user_id = false ) {
+ if ( !$_blog_id )
+ $_blog_id = $this->blog_id();
+
+ if ( !$_user_id )
+ $_user_id = $this->user_id();
+
+ $connections = $this->get_connections( $service_name, $_blog_id, $_user_id );
+ return ( is_array( $connections ) && count( $connections ) > 0 ? true : false );
+ }
+
+ /**
+ * Generates a connection URL.
+ *
+ * This is the URL, which, when visited by the user, starts the authentication
+ * process required to forge a connection.
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @return string
+ */
+ abstract function connect_url( $service_name );
+
+ /**
+ * Generates a Connection refresh URL.
+ *
+ * This is the URL, which, when visited by the user, re-authenticates their
+ * connection to the service.
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @return string
+ */
+ abstract function refresh_url( $service_name );
+
+ /**
+ * Generates a disconnection URL.
+ *
+ * This is the URL, which, when visited by the user, breaks their connection
+ * with the service.
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @param string $connection_id Connection ID
+ * @return string
+ */
+ abstract function disconnect_url( $service_name, $connection_id );
+
+ /**
+ * Returns a display name for the Service
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @return string
+ */
+ public static function get_service_label( $service_name ) {
+ switch ( $service_name ) {
+ case 'linkedin':
+ return 'LinkedIn';
+ break;
+ case 'twitter':
+ case 'facebook':
+ case 'tumblr':
+ default:
+ return ucfirst( $service_name );
+ break;
+ }
+ }
+
+/*
+ * Connections: For each Service, there can be multiple connections
+ * for a given user. For example, one user could be connected to Twitter
+ * as both @jetpack and as @wordpressdotcom
+ *
+ * For historical reasons, Connections are represented as an object
+ * on WordPress.com and as an array in Jetpack.
+ */
+
+ /**
+ * Get the active Connections of a Service
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
+ * @param false|int $_user_id The user ID. Use false (default) for the current user
+ * @return false|object[]|array[] false if no connections exist
+ */
+ abstract function get_connections( $service_name, $_blog_id = false, $_user_id = false );
+
+ /**
+ * Get a single Connection of a Service
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @param string $connection_id Connection ID
+ * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
+ * @param false|int $_user_id The user ID. Use false (default) for the current user
+ * @return false|object[]|array[] false if no connections exist
+ */
+ abstract function get_connection( $service_name, $connection_id, $_blog_id = false, $_user_id = false );
+
+ /**
+ * Get the Connection ID.
+ *
+ * Note that this is different than the Connection's uniqueid.
+ *
+ * Via a quirk of history, ID is globally unique and unique_id
+ * is only unique per site.
+ *
+ * @param object|array The Connection object (WordPress.com) or array (Jetpack)
+ * @return string
+ */
+ abstract function get_connection_id( $connection );
+
+ /**
+ * Get the Connection unique_id
+ *
+ * Note that this is different than the Connections ID.
+ *
+ * Via a quirk of history, ID is globally unique and unique_id
+ * is only unique per site.
+ *
+ * @param object|array The Connection object (WordPress.com) or array (Jetpack)
+ * @return string
+ */
+ abstract function get_connection_unique_id( $connection );
+
+ /**
+ * Get the Connection's Meta data
+ *
+ * @param object|array Connection
+ * @return array Connection Meta
+ */
+ abstract function get_connection_meta( $connection );
+
+ /**
+ * Disconnect a Connection
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @param string $connection_id Connection ID
+ * @param false|int $_blog_id The blog ID. Use false (default) for the current blog
+ * @param false|int $_user_id The user ID. Use false (default) for the current user
+ * @param bool $force_delete Whether to skip permissions checks
+ * @return false|void False on failure. Void on success.
+ */
+ abstract function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false );
+
+ /**
+ * Globalizes a Connection
+ *
+ * @param string $connection_id Connection ID
+ * @return bool Falsey on failure. Truthy on success.
+ */
+ abstract function globalize_connection( $connection_id );
+
+ /**
+ * Unglobalizes a Connection
+ *
+ * @param string $connection_id Connection ID
+ * @return bool Falsey on failure. Truthy on success.
+ */
+ abstract function unglobalize_connection( $connection_id );
+
+ /**
+ * Returns an external URL to the Connection's profile
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @param object|array The Connection object (WordPress.com) or array (Jetpack)
+ * @return false|string False on failure. URL on success.
+ */
+ function get_profile_link( $service_name, $connection ) {
+ $cmeta = $this->get_connection_meta( $connection );
+
+ if ( isset( $cmeta['connection_data']['meta']['link'] ) ) {
+ if ( 'facebook' == $service_name && 0 === strpos( parse_url( $cmeta['connection_data']['meta']['link'], PHP_URL_PATH ), '/app_scoped_user_id/' ) ) {
+ // App-scoped Facebook user IDs are not usable profile links
+ return false;
+ }
+
+ return $cmeta['connection_data']['meta']['link'];
+ } elseif ( 'facebook' == $service_name && isset( $cmeta['connection_data']['meta']['facebook_page'] ) ) {
+ return 'https://facebook.com/' . $cmeta['connection_data']['meta']['facebook_page'];
+ } elseif ( 'tumblr' == $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
+ return 'http://' . $cmeta['connection_data']['meta']['tumblr_base_hostname'];
+ } elseif ( 'twitter' == $service_name ) {
+ return 'https://twitter.com/' . substr( $cmeta['external_display'], 1 ); // Has a leading '@'
+ } else if ( 'linkedin' == $service_name ) {
+ if ( !isset( $cmeta['connection_data']['meta']['profile_url'] ) ) {
+ return false;
+ }
+
+ $profile_url_query = parse_url( $cmeta['connection_data']['meta']['profile_url'], PHP_URL_QUERY );
+ wp_parse_str( $profile_url_query, $profile_url_query_args );
+ if ( isset( $profile_url_query_args['key'] ) ) {
+ $id = $profile_url_query_args['key'];
+ } elseif ( isset( $profile_url_query_args['id'] ) ) {
+ $id = $profile_url_query_args['id'];
+ } else {
+ return false;
+ }
+
+ return esc_url_raw( add_query_arg( 'id', urlencode( $id ), 'http://www.linkedin.com/profile/view' ) );
+ } else {
+ return false; // no fallback. we just won't link it
+ }
+ }
+
+ /**
+ * Returns a display name for the Connection
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @param object|array The Connection object (WordPress.com) or array (Jetpack)
+ * @return string
+ */
+ function get_display_name( $service_name, $connection ) {
+ $cmeta = $this->get_connection_meta( $connection );
+
+ if ( isset( $cmeta['connection_data']['meta']['display_name'] ) ) {
+ return $cmeta['connection_data']['meta']['display_name'];
+ } elseif ( $service_name == 'tumblr' && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
+ return $cmeta['connection_data']['meta']['tumblr_base_hostname'];
+ } elseif ( $service_name == 'twitter' ) {
+ return $cmeta['external_display'];
+ } else {
+ $connection_display = $cmeta['external_display'];
+ if ( empty( $connection_display ) )
+ $connection_display = $cmeta['external_name'];
+ return $connection_display;
+ }
+ }
+
+ /**
+ * Whether the user needs to select additional options after connecting
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @param object|array The Connection object (WordPress.com) or array (Jetpack)
+ * @return bool
+ */
+ function show_options_popup( $service_name, $connection ) {
+ $cmeta = $this->get_connection_meta( $connection );
+
+ // always show if no selection has been made for facebook
+ if ( 'facebook' == $service_name && empty( $cmeta['connection_data']['meta']['facebook_profile'] ) && empty( $cmeta['connection_data']['meta']['facebook_page'] ) )
+ return true;
+
+ // always show if no selection has been made for tumblr
+ if ( 'tumblr' == $service_name && empty ( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) )
+ return true;
+
+ // if we have the specific connection info..
+ if ( isset( $_GET['id'] ) ) {
+ if ( $cmeta['connection_data']['id'] == $_GET['id'] )
+ return true;
+ } else {
+ // otherwise, just show if this is the completed step / first load
+ if ( !empty( $_GET['action'] ) && 'completed' == $_GET['action'] && !empty( $_GET['service'] ) && $service_name == $_GET['service'] && ! in_array( $_GET['service'], array( 'facebook', 'tumblr' ) ) )
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Whether the Connection is "valid" wrt Facebook's requirements.
+ *
+ * Must be connected to a Page (not a Profile).
+ * (Also returns true if we're in the middle of the connection process)
+ *
+ * @param object|array The Connection object (WordPress.com) or array (Jetpack)
+ * @return bool
+ */
+ function is_valid_facebook_connection( $connection ) {
+ if ( $this->is_connecting_connection( $connection ) ) {
+ return true;
+ }
+ $connection_meta = $this->get_connection_meta( $connection );
+ $connection_data = $connection_meta['connection_data'];
+ return isset( $connection_data[ 'meta' ][ 'facebook_page' ] );
+ }
+
+ /**
+ * LinkedIn needs to be reauthenticated to use v2 of their API.
+ * If it's using LinkedIn old API, it's an 'invalid' connection
+ *
+ * @param object|array The Connection object (WordPress.com) or array (Jetpack)
+ * @return bool
+ */
+ function is_invalid_linkedin_connection( $connection ) {
+ // LinkedIn API v1 included the profile link in the connection data.
+ $connection_meta = $this->get_connection_meta( $connection );
+ return isset( $connection_meta['connection_data']['meta']['profile_url'] );
+ }
+
+ /**
+ * Whether the Connection currently being connected
+ *
+ * @param object|array The Connection object (WordPress.com) or array (Jetpack)
+ * @return bool
+ */
+ function is_connecting_connection( $connection ) {
+ $connection_meta = $this->get_connection_meta( $connection );
+ $connection_data = $connection_meta['connection_data'];
+ return isset( $connection_data[ 'meta' ]['options_responses'] );
+ }
+
+ /**
+ * AJAX Handler to run connection tests on all Connections
+ * @return void
+ */
+ function test_publicize_conns() {
+ wp_send_json_success( $this->get_publicize_conns_test_results() );
+ }
+
+ /**
+ * Run connection tests on all Connections
+ *
+ * @return array {
+ * Array of connection test results.
+ *
+ * @type string 'connectionID' Connection identifier string that is unique for each connection
+ * @type string 'serviceName' Slug of the connection's service (facebook, twitter, ...)
+ * @type bool 'connectionTestPassed' Whether the connection test was successful
+ * @type string 'connectionTestMessage' Test success or error message
+ * @type bool 'userCanRefresh' Whether the user can re-authenticate their connection to the service
+ * @type string 'refreshText' Message instructing user to re-authenticate their connection to the service
+ * @type string 'refreshURL' URL, which, when visited by the user, re-authenticates their connection to the service.
+ * @type string 'unique_id' ID string representing connection
+ * }
+ */
+ function get_publicize_conns_test_results() {
+ $test_results = array();
+
+ foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
+ foreach ( $connections as $connection ) {
+
+ $id = $this->get_connection_id( $connection );
+
+ $connection_test_passed = true;
+ $connection_test_message = __( 'This connection is working correctly.' , 'jetpack' );
+ $user_can_refresh = false;
+ $refresh_text = '';
+ $refresh_url = '';
+
+ $connection_test_result = true;
+ if ( method_exists( $this, 'test_connection' ) ) {
+ $connection_test_result = $this->test_connection( $service_name, $connection );
+ }
+
+ if ( is_wp_error( $connection_test_result ) ) {
+ $connection_test_passed = false;
+ $connection_test_message = $connection_test_result->get_error_message();
+ $error_data = $connection_test_result->get_error_data();
+
+ $user_can_refresh = $error_data['user_can_refresh'];
+ $refresh_text = $error_data['refresh_text'];
+ $refresh_url = $error_data['refresh_url'];
+ }
+ // Mark facebook profiles as deprecated
+ if ( 'facebook' === $service_name ) {
+ if ( ! $this->is_valid_facebook_connection( $connection ) ) {
+ $connection_test_passed = false;
+ $user_can_refresh = false;
+ $connection_test_message = __( 'Please select a Facebook Page to publish updates.', 'jetpack' );
+ }
+ }
+
+ // LinkedIn needs reauthentication to be compatible with v2 of their API
+ if ( 'linkedin' === $service_name && $this->is_invalid_linkedin_connection( $connection ) ) {
+ $connection_test_passed = 'must_reauth';
+ $user_can_refresh = false;
+ $connection_test_message = esc_html__( 'Your LinkedIn connection needs to be reauthenticated to continue working – head to Sharing to take care of it.', 'jetpack' );
+ }
+
+ $unique_id = null;
+ if ( ! empty( $connection->unique_id ) ) {
+ $unique_id = $connection->unique_id;
+ } else if ( ! empty( $connection['connection_data']['token_id'] ) ) {
+ $unique_id = $connection['connection_data']['token_id'];
+ }
+
+ $test_results[] = array(
+ 'connectionID' => $id,
+ 'serviceName' => $service_name,
+ 'connectionTestPassed' => $connection_test_passed,
+ 'connectionTestMessage' => esc_attr( $connection_test_message ),
+ 'userCanRefresh' => $user_can_refresh,
+ 'refreshText' => esc_attr( $refresh_text ),
+ 'refreshURL' => $refresh_url,
+ 'unique_id' => $unique_id,
+ );
+ }
+ }
+
+ return $test_results;
+ }
+
+ /**
+ * Run the connection test for the Connection
+ *
+ * @param string $service_name 'facebook', 'twitter', etc.
+ * @param object|array The Connection object (WordPress.com) or array (Jetpack)
+ * @return WP_Error|true WP_Error on failure. True on success
+ */
+ abstract function test_connection( $service_name, $connection );
+
+ /**
+ * Retrieves current list of connections and applies filters.
+ *
+ * Retrieves current available connections and checks if the connections
+ * have already been used to share current post. Finally, the checkbox
+ * form UI fields are calculated. This function exposes connection form
+ * data directly as array so it can be retrieved for static HTML generation
+ * or JSON consumption.
+ *
+ * @since 6.7.0
+ *
+ * @param integer $selected_post_id Optional. Post ID to query connection status for.
+ *
+ * @return array {
+ * Array of UI setup data for connection list form.
+ *
+ * @type string 'unique_id' ID string representing connection
+ * @type string 'service_name' Slug of the connection's service (facebook, twitter, ...)
+ * @type string 'service_label' Service Label (Facebook, Twitter, ...)
+ * @type string 'display_name' Connection's human-readable Username: "@jetpack"
+ * @type bool 'enabled' Default value for the connection (e.g., for a checkbox).
+ * @type bool 'done' Has this connection already been publicized to?
+ * @type bool 'toggleable' Is the user allowed to change the value for the connection?
+ * @type bool 'global' Is this connection a global one?
+ * }
+ */
+ public function get_filtered_connection_data( $selected_post_id = null ) {
+ $connection_list = array();
+
+ $post = get_post( $selected_post_id ); // Defaults to current post if $post_id is null.
+ // Handle case where there is no current post.
+ if ( ! empty( $post ) ) {
+ $post_id = $post->ID;
+ } else {
+ $post_id = null;
+ }
+
+ $services = $this->get_services( 'connected' );
+ $all_done = $this->post_is_done_sharing( $post_id );
+
+ // We don't allow Publicizing to the same external id twice, to prevent spam.
+ $service_id_done = (array) get_post_meta( $post_id, $this->POST_SERVICE_DONE, true );
+
+ foreach ( $services as $service_name => $connections ) {
+ foreach ( $connections as $connection ) {
+ $connection_meta = $this->get_connection_meta( $connection );
+ $connection_data = $connection_meta['connection_data'];
+
+ $unique_id = $this->get_connection_unique_id( $connection );
+
+
+ // Was this connection (OR, old-format service) already Publicized to?
+ $done = ! empty( $post ) && (
+ // New flags
+ 1 == get_post_meta( $post->ID, $this->POST_DONE . $unique_id, true )
+ ||
+ // old flags
+ 1 == get_post_meta( $post->ID, $this->POST_DONE . $service_name, true )
+ );
+
+ /**
+ * Filter whether a post should be publicized to a given service.
+ *
+ * @module publicize
+ *
+ * @since 2.0.0
+ *
+ * @param bool true Should the post be publicized to a given service? Default to true.
+ * @param int $post_id Post ID.
+ * @param string $service_name Service name.
+ * @param array $connection_data Array of information about all Publicize details for the site.
+ */
+ if ( ! apply_filters( 'wpas_submit_post?', true, $post_id, $service_name, $connection_data ) ) {
+ continue;
+ }
+
+ // Should we be skipping this one?
+ $skip = (
+ (
+ ! empty( $post )
+ &&
+ in_array( $post->post_status, array( 'publish', 'draft', 'future' ) )
+ &&
+ (
+ // New flags
+ get_post_meta( $post->ID, $this->POST_SKIP . $unique_id, true )
+ ||
+ // Old flags
+ get_post_meta( $post->ID, $this->POST_SKIP . $service_name )
+ )
+ )
+ ||
+ (
+ is_array( $connection )
+ &&
+ isset( $connection_meta['external_id'] ) && ! empty( $service_id_done[ $service_name ][ $connection_meta['external_id'] ] )
+ )
+ );
+
+ // If this one has already been publicized to, don't let it happen again.
+ $toggleable = ! $done && ! $all_done;
+
+ // Determine the state of the checkbox (on/off) and allow filtering.
+ $enabled = $done || ! $skip;
+ /**
+ * Filter the checkbox state of each Publicize connection appearing in the post editor.
+ *
+ * @module publicize
+ *
+ * @since 2.0.1
+ *
+ * @param bool $enabled Should the Publicize checkbox be enabled for a given service.
+ * @param int $post_id Post ID.
+ * @param string $service_name Service name.
+ * @param array $connection Array of connection details.
+ */
+ $enabled = apply_filters( 'publicize_checkbox_default', $enabled, $post_id, $service_name, $connection );
+
+ /**
+ * If this is a global connection and this user doesn't have enough permissions to modify
+ * those connections, don't let them change it.
+ */
+ if ( ! $done && ( 0 == $connection_data['user_id'] && ! current_user_can( $this->GLOBAL_CAP ) ) ) {
+ $toggleable = false;
+
+ /**
+ * Filters the checkboxes for global connections with non-prilvedged users.
+ *
+ * @module publicize
+ *
+ * @since 3.7.0
+ *
+ * @param bool $enabled Indicates if this connection should be enabled. Default true.
+ * @param int $post_id ID of the current post
+ * @param string $service_name Name of the connection (Facebook, Twitter, etc)
+ * @param array $connection Array of data about the connection.
+ */
+ $enabled = apply_filters( 'publicize_checkbox_global_default', $enabled, $post_id, $service_name, $connection );
+ }
+
+ // Force the checkbox to be checked if the post was DONE, regardless of what the filter does.
+ if ( $done ) {
+ $enabled = true;
+ }
+
+ $connection_list[] = array(
+ 'unique_id' => $unique_id,
+ 'service_name' => $service_name,
+ 'service_label' => $this->get_service_label( $service_name ),
+ 'display_name' => $this->get_display_name( $service_name, $connection ),
+
+ 'enabled' => $enabled,
+ 'done' => $done,
+ 'toggleable' => $toggleable,
+ 'global' => 0 == $connection_data['user_id'],
+ );
+ }
+ }
+
+ return $connection_list;
+ }
+
+ /**
+ * Checks if post has already been shared by Publicize in the past.
+ *
+ * @since 6.7.0
+ *
+ * @param integer $post_id Optional. Post ID to query connection status for: will use current post if missing.
+ *
+ * @return bool True if post has already been shared by Publicize, false otherwise.
+ */
+ abstract public function post_is_done_sharing( $post_id = null );
+
+ /**
+ * Retrieves full list of available Publicize connection services.
+ *
+ * Retrieves current available publicize service connections
+ * with associated labels and URLs.
+ *
+ * @since 6.7.0
+ *
+ * @return array {
+ * Array of UI service connection data for all services
+ *
+ * @type string 'name' Name of service.
+ * @type string 'label' Display label for service.
+ * @type string 'url' URL for adding connection to service.
+ * }
+ */
+ function get_available_service_data() {
+ $available_services = $this->get_services( 'all' );
+ $available_service_data = array();
+
+ foreach ( $available_services as $service_name => $service ) {
+ $available_service_data[] = array(
+ 'name' => $service_name,
+ 'label' => $this->get_service_label( $service_name ),
+ 'url' => $this->connect_url( $service_name ),
+ );
+ }
+
+ return $available_service_data;
+ }
+
+/*
+ * Site Data
+ */
+
+ function user_id() {
+ return get_current_user_id();
+ }
+
+ function blog_id() {
+ return get_current_blog_id();
+ }
+
+/*
+ * Posts
+ */
+
+ /**
+ * Checks old and new status to see if the post should be flagged as
+ * ready to Publicize.
+ *
+ * Attached to the `transition_post_status` filter.
+ *
+ * @param string $new_status
+ * @param string $old_status
+ * @param WP_Post $post
+ * @return void
+ */
+ abstract function flag_post_for_publicize( $new_status, $old_status, $post );
+
+ /**
+ * Ensures the Post internal post-type supports `publicize`
+ *
+ * This feature support flag is used by the REST API.
+ */
+ function add_post_type_support() {
+ add_post_type_support( 'post', 'publicize' );
+ }
+
+ /**
+ * Register the Publicize Gutenberg extension
+ */
+ function register_gutenberg_extension() {
+ // TODO: The `gutenberg/available-extensions` endpoint currently doesn't accept a post ID,
+ // so we cannot pass one to `$this->current_user_can_access_publicize_data()`.
+
+ if ( $this->current_user_can_access_publicize_data() ) {
+ Jetpack_Gutenberg::set_extension_available( 'jetpack/publicize' );
+ } else {
+ Jetpack_Gutenberg::set_extension_unavailable( 'jetpack/publicize', 'unauthorized' );
+
+ }
+ }
+
+ /**
+ * Can the current user access Publicize Data.
+ *
+ * @param int $post_id. 0 for general access. Post_ID for specific access.
+ * @return bool
+ */
+ function current_user_can_access_publicize_data( $post_id = 0 ) {
+ /**
+ * Filter what user capability is required to use the publicize form on the edit post page. Useful if publish post capability has been removed from role.
+ *
+ * @module publicize
+ *
+ * @since 4.1.0
+ *
+ * @param string $capability User capability needed to use publicize
+ */
+ $capability = apply_filters( 'jetpack_publicize_capability', 'publish_posts' );
+
+ if ( 'publish_posts' === $capability && $post_id ) {
+ return current_user_can( 'publish_post', $post_id );
+ }
+
+ return current_user_can( $capability );
+ }
+
+ /**
+ * Auth callback for the protected ->POST_MESS post_meta
+ *
+ * @param bool $allowed
+ * @param string $meta_key
+ * @param int $object_id Post ID
+ * @return bool
+ */
+ function message_meta_auth_callback( $allowed, $meta_key, $object_id ) {
+ return $this->current_user_can_access_publicize_data( $object_id );
+ }
+
+ /**
+ * Registers the ->POST_MESS post_meta for use in the REST API.
+ *
+ * Registers for each post type that with `publicize` feature support.
+ */
+ function register_post_meta() {
+ $args = array(
+ 'type' => 'string',
+ 'description' => __( 'The message to use instead of the title when sharing to Publicize Services', 'jetpack' ),
+ 'single' => true,
+ 'default' => '',
+ 'show_in_rest' => array(
+ 'name' => 'jetpack_publicize_message'
+ ),
+ 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
+ );
+
+ foreach ( get_post_types() as $post_type ) {
+ if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
+ continue;
+ }
+
+ $args['object_subtype'] = $post_type;
+
+ register_meta( 'post', $this->POST_MESS, $args );
+ }
+ }
+
+ /**
+ * Fires when a post is saved, checks conditions and saves state in postmeta so that it
+ * can be picked up later by @see ::publicize_post() on WordPress.com codebase.
+ *
+ * Attached to the `save_post` action.
+ *
+ * @param int $post_id
+ * @param WP_Post $post
+ * @return void
+ */
+ function save_meta( $post_id, $post ) {
+ $cron_user = null;
+ $submit_post = true;
+
+ if ( ! $this->post_type_is_publicizeable( $post->post_type ) )
+ return;
+
+ // Don't Publicize during certain contexts:
+
+ // - import
+ if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
+ $submit_post = false;
+ }
+
+ // - on quick edit, autosave, etc but do fire on p2, quickpress, and instapost ajax
+ if (
+ defined( 'DOING_AJAX' )
+ &&
+ DOING_AJAX
+ &&
+ !did_action( 'p2_ajax' )
+ &&
+ !did_action( 'wp_ajax_json_quickpress_post' )
+ &&
+ !did_action( 'wp_ajax_instapost_publish' )
+ &&
+ !did_action( 'wp_ajax_post_reblog' )
+ &&
+ !did_action( 'wp_ajax_press-this-save-post' )
+ ) {
+ $submit_post = false;
+ }
+
+ // - bulk edit
+ if ( isset( $_GET['bulk_edit'] ) ) {
+ $submit_post = false;
+ }
+
+ // - API/XML-RPC Test Posts
+ if (
+ (
+ defined( 'XMLRPC_REQUEST' )
+ &&
+ XMLRPC_REQUEST
+ ||
+ defined( 'APP_REQUEST' )
+ &&
+ APP_REQUEST
+ )
+ &&
+ 0 === strpos( $post->post_title, 'Temporary Post Used For Theme Detection' )
+ ) {
+ $submit_post = false;
+ }
+
+ // only work with certain statuses (avoids inherits, auto drafts etc)
+ if ( !in_array( $post->post_status, array( 'publish', 'draft', 'future' ) ) ) {
+ $submit_post = false;
+ }
+
+ // don't publish password protected posts
+ if ( '' !== $post->post_password ) {
+ $submit_post = false;
+ }
+
+ // Did this request happen via wp-admin?
+ $from_web = isset( $_SERVER['REQUEST_METHOD'] )
+ &&
+ 'post' == strtolower( $_SERVER['REQUEST_METHOD'] )
+ &&
+ isset( $_POST[$this->ADMIN_PAGE] );
+
+ if ( ( $from_web || defined( 'POST_BY_EMAIL' ) ) && isset( $_POST['wpas_title'] ) ) {
+ if ( empty( $_POST['wpas_title'] ) ) {
+ delete_post_meta( $post_id, $this->POST_MESS );
+ } else {
+ update_post_meta( $post_id, $this->POST_MESS, trim( stripslashes( $_POST['wpas_title'] ) ) );
+ }
+ }
+
+ // change current user to provide context for get_services() if we're running during cron
+ if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
+ $cron_user = (int) $GLOBALS['user_ID'];
+ wp_set_current_user( $post->post_author );
+ }
+
+ /**
+ * In this phase, we mark connections that we want to SKIP. When Publicize is actually triggered,
+ * it will Publicize to everything *except* those marked for skipping.
+ */
+ foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
+ foreach ( $connections as $connection ) {
+ $connection_data = '';
+ if ( method_exists( $connection, 'get_meta' ) )
+ $connection_data = $connection->get_meta( 'connection_data' );
+ elseif ( ! empty( $connection['connection_data'] ) )
+ $connection_data = $connection['connection_data'];
+
+ /** This action is documented in modules/publicize/ui.php */
+ if ( false == apply_filters( 'wpas_submit_post?', $submit_post, $post_id, $service_name, $connection_data ) ) {
+ delete_post_meta( $post_id, $this->PENDING );
+ continue;
+ }
+
+ if ( !empty( $connection->unique_id ) )
+ $unique_id = $connection->unique_id;
+ else if ( !empty( $connection['connection_data']['token_id'] ) )
+ $unique_id = $connection['connection_data']['token_id'];
+
+ // This was a wp-admin request, so we need to check the state of checkboxes
+ if ( $from_web ) {
+ // delete stray service-based post meta
+ delete_post_meta( $post_id, $this->POST_SKIP . $service_name );
+
+ // We *unchecked* this stream from the admin page, or it's set to readonly, or it's a new addition
+ if ( empty( $_POST[$this->ADMIN_PAGE]['submit'][$unique_id] ) ) {
+ // Also make sure that the service-specific input isn't there.
+ // If the user connected to a new service 'in-page' then a hidden field with the service
+ // name is added, so we just assume they wanted to Publicize to that service.
+ if ( empty( $_POST[$this->ADMIN_PAGE]['submit'][$service_name] ) ) {
+ // Nothing seems to be checked, so we're going to mark this one to be skipped
+ update_post_meta( $post_id, $this->POST_SKIP . $unique_id, 1 );
+ continue;
+ } else {
+ // clean up any stray post meta
+ delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
+ }
+ } else {
+ // The checkbox for this connection is explicitly checked -- make sure we DON'T skip it
+ delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
+ }
+ }
+
+ /**
+ * Fires right before the post is processed for Publicize.
+ * Users may hook in here and do anything else they need to after meta is written,
+ * and before the post is processed for Publicize.
+ *
+ * @since 2.1.2
+ *
+ * @param bool $submit_post Should the post be publicized.
+ * @param int $post->ID Post ID.
+ * @param string $service_name Service name.
+ * @param array $connection Array of connection details.
+ */
+ do_action( 'publicize_save_meta', $submit_post, $post_id, $service_name, $connection );
+ }
+ }
+
+ if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
+ wp_set_current_user( $cron_user );
+ }
+
+ // Next up will be ::publicize_post()
+ }
+
+ /**
+ * Alters the "Post Published" message to include information about where the post
+ * was Publicized to.
+ *
+ * Attached to the `post_updated_messages` filter
+ *
+ * @param string[] $messages
+ * @return string[]
+ */
+ public function update_published_message( $messages ) {
+ global $post_type, $post_type_object, $post;
+ if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
+ return $messages;
+ }
+
+ // Bail early if the post is private.
+ if ( 'publish' !== $post->post_status ) {
+ return $messages;
+ }
+
+ $view_post_link_html = '';
+ $viewable = is_post_type_viewable( $post_type_object );
+ if ( $viewable ) {
+ $view_text = esc_html__( 'View post' ); // intentionally omitted domain
+
+ if ( 'jetpack-portfolio' == $post_type ) {
+ $view_text = esc_html__( 'View project', 'jetpack' );
+ }
+
+ $view_post_link_html = sprintf( ' <a href="%1$s">%2$s</a>',
+ esc_url( get_permalink( $post ) ),
+ $view_text
+ );
+ }
+
+ $services = $this->get_publicizing_services( $post->ID );
+ if ( empty( $services ) ) {
+ return $messages;
+ }
+
+ $labels = array();
+ foreach ( $services as $service_name => $display_names ) {
+ $labels[] = sprintf(
+ /* translators: Service name is %1$s, and account name is %2$s. */
+ esc_html__( '%1$s (%2$s)', 'jetpack' ),
+ esc_html( $service_name ),
+ esc_html( implode( ', ', $display_names ) )
+ );
+ }
+
+ $messages['post'][6] = sprintf(
+ /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
+ esc_html__( 'Post published and sharing on %1$s.', 'jetpack' ),
+ implode( ', ', $labels )
+ ) . $view_post_link_html;
+
+ if ( $post_type == 'post' && class_exists('Jetpack_Subscriptions' ) ) {
+ $subscription = Jetpack_Subscriptions::init();
+ if ( $subscription->should_email_post_to_subscribers( $post ) ) {
+ $messages['post'][6] = sprintf(
+ /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
+ esc_html__( 'Post published, sending emails to subscribers and sharing post on %1$s.', 'jetpack' ),
+ implode( ', ', $labels )
+ ) . $view_post_link_html;
+ }
+ }
+
+ $messages['jetpack-portfolio'][6] = sprintf(
+ /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
+ esc_html__( 'Project published and sharing project on %1$s.', 'jetpack' ),
+ implode( ', ', $labels )
+ ) . $view_post_link_html;
+
+ return $messages;
+ }
+
+ /**
+ * Get the Connections the Post was just Publicized to.
+ *
+ * Only reliable just after the Post was published.
+ *
+ * @param int $post_id
+ * @return string[] Array of Service display name => Connection display name
+ */
+ function get_publicizing_services( $post_id ) {
+ $services = array();
+
+ foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
+ // services have multiple connections.
+ foreach ( $connections as $connection ) {
+ $unique_id = '';
+ if ( ! empty( $connection->unique_id ) )
+ $unique_id = $connection->unique_id;
+ else if ( ! empty( $connection['connection_data']['token_id'] ) )
+ $unique_id = $connection['connection_data']['token_id'];
+
+ // Did we skip this connection?
+ if ( get_post_meta( $post_id, $this->POST_SKIP . $unique_id, true ) ) {
+ continue;
+ }
+ $services[ $this->get_service_label( $service_name ) ][] = $this->get_display_name( $service_name, $connection );
+ }
+ }
+
+ return $services;
+ }
+
+ /**
+ * Is the post Publicize-able?
+ *
+ * Only valid prior to Publicizing a Post.
+ *
+ * @param WP_Post $post
+ * @return bool
+ */
+ function post_is_publicizeable( $post ) {
+ if ( ! $this->post_type_is_publicizeable( $post->post_type ) )
+ return false;
+
+ // This is more a precaution. To only publicize posts that are published. (Mostly relevant for Jetpack sites)
+ if ( 'publish' !== $post->post_status ) {
+ return false;
+ }
+
+ // If it's not flagged as ready, then abort. @see ::flag_post_for_publicize()
+ if ( ! get_post_meta( $post->ID, $this->PENDING, true ) )
+ return false;
+
+ return true;
+ }
+
+ /**
+ * Is a given post type Publicize-able?
+ *
+ * Not every CPT lends itself to Publicize-ation. Allow CPTs to register by adding their CPT via
+ * the publicize_post_types array filter.
+ *
+ * @param string $post_type The post type to check.
+ * @return bool True if the post type can be Publicized.
+ */
+ function post_type_is_publicizeable( $post_type ) {
+ if ( 'post' == $post_type )
+ return true;
+
+ return post_type_supports( $post_type, 'publicize' );
+ }
+
+ /**
+ * Already-published posts should not be Publicized by default. This filter sets checked to
+ * false if a post has already been published.
+ *
+ * Attached to the `publicize_checkbox_default` filter
+ *
+ * @param bool $checked
+ * @param int $post_id
+ * @param string $service_name 'facebook', 'twitter', etc
+ * @param object|array The Connection object (WordPress.com) or array (Jetpack)
+ * @return bool
+ */
+ function publicize_checkbox_default( $checked, $post_id, $service_name, $connection ) {
+ if ( 'publish' == get_post_status( $post_id ) ) {
+ return false;
+ }
+
+ return $checked;
+ }
+
+/*
+ * Util
+ */
+
+ /**
+ * Converts a Publicize message template string into a sprintf format string
+ *
+ * @param string[] $args
+ * 0 - The Publicize message template: 'Check out my post: %title% @ %url'
+ * ... - The template tags 'title', 'url', etc.
+ * @return string
+ */
+ protected static function build_sprintf( $args ) {
+ $search = array();
+ $replace = array();
+ foreach ( $args as $k => $arg ) {
+ if ( 0 == $k ) {
+ $string = $arg;
+ continue;
+ }
+ $search[] = "%$arg%";
+ $replace[] = "%$k\$s";
+ }
+ return str_replace( $search, $replace, $string );
+ }
+}
+
+function publicize_calypso_url() {
+ $calypso_sharing_url = 'https://wordpress.com/marketing/connections/';
+ if ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'build_raw_urls' ) ) {
+ $site_suffix = Jetpack::build_raw_urls( home_url() );
+ } elseif ( class_exists( 'WPCOM_Masterbar' ) && method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
+ $site_suffix = WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
+ }
+
+ if ( $site_suffix ) {
+ return $calypso_sharing_url . $site_suffix;
+ } else {
+ return $calypso_sharing_url;
+ }
+}
diff --git a/plugins/jetpack/modules/publicize/ui.php b/plugins/jetpack/modules/publicize/ui.php
new file mode 100644
index 00000000..43a6c05f
--- /dev/null
+++ b/plugins/jetpack/modules/publicize/ui.php
@@ -0,0 +1,657 @@
+<?php
+
+/**
+* Only user facing pieces of Publicize are found here.
+*/
+class Publicize_UI {
+
+ /**
+ * Contains an instance of class 'publicize' which loads Keyring, sets up services, etc.
+ */
+ public $publicize;
+
+ /**
+ * @var string URL to Sharing settings page in wordpress.com
+ */
+ protected $publicize_settings_url = '';
+
+ /**
+ * Hooks into WordPress to display the various pieces of UI and load our assets
+ */
+ function __construct() {
+ global $publicize;
+
+ $this->publicize = $publicize = new Publicize;
+
+ add_action( 'init', array( $this, 'init' ) );
+ }
+
+ function init() {
+ $this->publicize_settings_url = publicize_calypso_url();
+
+ // Show only to users with the capability required to manage their Publicize connections.
+ if ( ! $this->publicize->current_user_can_access_publicize_data() ) {
+ return;
+ }
+
+ // assets (css, js)
+ if ( $this->in_jetpack ) {
+ add_action( 'load-settings_page_sharing', array( $this, 'load_assets' ) );
+ }
+ add_action( 'admin_head-post.php', array( $this, 'post_page_metabox_assets' ) );
+ add_action( 'admin_head-post-new.php', array( $this, 'post_page_metabox_assets' ) );
+
+ // management of publicize (sharing screen, ajax/lightbox popup, and metabox on post screen)
+ add_action( 'pre_admin_screen_sharing', array( $this, 'admin_page' ) );
+ add_action( 'post_submitbox_misc_actions', array( $this, 'post_page_metabox' ) );
+ }
+
+ /**
+ * If the ShareDaddy plugin is not active we need to add the sharing settings page to the menu still
+ */
+ function sharing_menu() {
+ add_submenu_page(
+ 'options-general.php',
+ esc_html__( 'Sharing Settings', 'jetpack' ),
+ esc_html__( 'Sharing', 'jetpack' ),
+ 'publish_posts',
+ 'sharing',
+ array( $this, 'wrapper_admin_page' )
+ );
+ }
+
+ function wrapper_admin_page() {
+ Jetpack_Admin_Page::wrap_ui( array( $this, 'management_page' ) );
+ }
+
+ /**
+ * Management page to load if Sharedaddy is not active so the 'pre_admin_screen_sharing' action exists.
+ */
+ function management_page() { ?>
+ <div class="wrap">
+ <div class="icon32" id="icon-options-general"><br /></div>
+ <h1><?php esc_html_e( 'Sharing Settings', 'jetpack' ); ?></h1>
+
+ <?php
+ /** This action is documented in modules/sharedaddy/sharing.php */
+ do_action( 'pre_admin_screen_sharing' );
+ ?>
+
+ </div> <?php
+ }
+
+ /**
+ * styling for the sharing screen and popups
+ * JS for the options and switching
+ */
+ function load_assets() {
+ Jetpack_Admin_Page::load_wrapper_styles();
+ }
+
+ /**
+ * Lists the current user's publicized accounts for the blog
+ * looks exactly like Publicize v1 for now, UI and functionality updates will come after the move to keyring
+ */
+ function admin_page() {
+ ?>
+ <h2 id="publicize"><?php esc_html_e( 'Publicize', 'jetpack' ) ?></h2>
+ <p><?php esc_html_e( 'Connect social media services to automatically share new posts.', 'jetpack' ) ?></p>
+ <h4><?php
+ printf(
+ wp_kses(
+ __( "We've made some updates to Publicize. Please visit the <a href='%s' class='jptracks' data-jptracks-name='legacy_publicize_settings'>WordPress.com sharing page</a> to manage your publicize connections or use the button below.", 'jetpack' ),
+ array( 'a' => array( 'href' => array(), 'class' => array(), 'data-jptracks-name' => array() ) )
+ ),
+ esc_url( publicize_calypso_url() )
+ );
+ ?>
+ </h4>
+
+ <a href="<?php echo esc_url( publicize_calypso_url() ); ?>" class="button button-primary jptracks" data-jptracks-name='legacy_publicize_settings'><?php esc_html_e( 'Publicize Settings', 'jetpack' ); ?></a>
+ <?php
+ }
+
+ /**
+ * CSS for styling the publicize message box and counter that displays on the post page.
+ * There is also some JavaScript for length counting and some basic display effects.
+ */
+ function post_page_metabox_assets() {
+ global $post;
+ $user_id = empty( $post->post_author ) ? $GLOBALS['user_ID'] : $post->post_author;
+
+ $default_prefix = $this->publicize->default_prefix;
+ $default_prefix = preg_replace( '/%([0-9])\$s/', "' + %\\1\$s + '", esc_js( $default_prefix ) );
+
+ $default_message = $this->publicize->default_message;
+ $default_message = preg_replace( '/%([0-9])\$s/', "' + %\\1\$s + '", esc_js( $default_message ) );
+
+ $default_suffix = $this->publicize->default_suffix;
+ $default_suffix = preg_replace( '/%([0-9])\$s/', "' + %\\1\$s + '", esc_js( $default_suffix ) );
+
+ $max_length = defined( 'JETPACK_PUBLICIZE_TWITTER_LENGTH' ) ? JETPACK_PUBLICIZE_TWITTER_LENGTH : 280;
+ $max_length = $max_length - 24; // t.co link, space
+
+ ?>
+
+<script type="text/javascript">
+jQuery( function($) {
+ var wpasTitleCounter = $( '#wpas-title-counter' ),
+ wpasTwitterCheckbox = $( '.wpas-submit-twitter' ).length,
+ postTitle = $( '#title' ),
+ wpasTitle = $( '#wpas-title' ).keyup( function() {
+ var postTitleVal,
+ length = wpasTitle.val().length;
+
+ if ( ! length ) {
+ length = wpasTitle.attr( 'placeholder' ).length;
+ }
+
+ wpasTitleCounter.text( length ).trigger( 'change' );
+ } ),
+ authClick = false;
+
+ wpasTitleCounter.on( 'change', function( e ) {
+ if ( wpasTwitterCheckbox && parseInt( $( e.currentTarget ).text(), 10 ) > <?php echo (int) $max_length; ?> ) {
+ wpasTitleCounter.addClass( 'wpas-twitter-length-limit' );
+ } else {
+ wpasTitleCounter.removeClass( 'wpas-twitter-length-limit' );
+ }
+ } );
+
+ // Keep the postTitle and the placeholder in sync
+ postTitle.on( 'keyup', function( e ) {
+ var url = $( '#sample-permalink' ).text();
+ var defaultMessage = $.trim( '<?php printf( $default_prefix, 'url' ); printf( $default_message, 'e.currentTarget.value', 'url' ); printf( $default_suffix, 'url' ); ?>' )
+ .replace( /<[^>]+>/g,'');
+
+ wpasTitle.attr( 'placeholder', defaultMessage );
+ wpasTitle.trigger( 'keyup' );
+ } );
+
+ // set the initial placeholder
+ postTitle.trigger( 'keyup' );
+
+ // If a custom message has been provided, open the UI so the author remembers
+ if ( wpasTitle.val() && ! wpasTitle.prop( 'disabled' ) && wpasTitle.attr( 'placeholder' ) !== wpasTitle.val() ) {
+ $( '#publicize-form' ).show();
+ $( '#publicize-defaults' ).hide();
+ $( '#publicize-form-edit' ).hide();
+ }
+
+ $('#publicize-disconnected-form-show').click( function() {
+ $('#publicize-form').slideDown( 'fast' );
+ $(this).hide();
+ } );
+
+ $('#publicize-disconnected-form-hide').click( function() {
+ $('#publicize-form').slideUp( 'fast' );
+ $('#publicize-disconnected-form-show').show();
+ } );
+
+ $('#publicize-form-edit').click( function() {
+ $('#publicize-form').slideDown( 'fast', function() {
+ var selBeg = 0, selEnd = 0;
+ wpasTitle.focus();
+
+ if ( ! wpasTitle.text() ) {
+ wpasTitle.text( wpasTitle.attr( 'placeholder' ) );
+
+ selBeg = wpasTitle.text().indexOf( postTitle.val() );
+ if ( selBeg < 0 ) {
+ selBeg = 0;
+ } else {
+ selEnd = selBeg + postTitle.val().length;
+ }
+
+ var domObj = wpasTitle.get(0);
+ if ( domObj.setSelectionRange ) {
+ domObj.setSelectionRange( selBeg, selEnd );
+ } else if ( domObj.createTextRange ) {
+ var r = domObj.createTextRange();
+ r.moveStart( 'character', selBeg );
+ r.moveEnd( 'character', selEnd );
+ r.select();
+ }
+ }
+ } );
+
+ $('#publicize-defaults').hide();
+ $(this).hide();
+ return false;
+ } );
+
+ $('#publicize-form-hide').click( function() {
+ var newList = $.map( $('#publicize-form').slideUp( 'fast' ).find( ':checked' ), function( el ) {
+ return $.trim( $(el).parent( 'label' ).text() );
+ } );
+ $('#publicize-defaults').html( '<strong>' + newList.join( '</strong>, <strong>' ) + '</strong>' ).show();
+ $('#publicize-form-edit').show();
+ return false;
+ } );
+
+ $('.authorize-link').click( function() {
+ if ( authClick ) {
+ return false;
+ }
+ authClick = true;
+ $(this).after( '<img src="images/loading.gif" class="alignleft" style="margin: 0 .5em" />' );
+ $.ajaxSetup( { async: false } );
+
+ if ( window.wp && window.wp.autosave ) {
+ window.wp.autosave.server.triggerSave();
+ } else {
+ autosave();
+ }
+
+ return true;
+ } );
+
+ $( '.pub-service' ).click( function() {
+ var service = $(this).data( 'service' ),
+ fakebox = '<input id="wpas-submit-' + service + '" type="hidden" value="1" name="wpas[submit][' + service + ']" />';
+ $( '#add-publicize-check' ).append( fakebox );
+ } );
+
+ publicizeConnTestStart = function() {
+ $( '#pub-connection-tests' )
+ .removeClass( 'below-h2' )
+ .removeClass( 'error' )
+ .removeClass( 'publicize-token-refresh-message' )
+ .addClass( 'test-in-progress' )
+ .html( '' );
+ $.post( ajaxurl, { action: 'test_publicize_conns' }, publicizeConnTestComplete );
+ }
+
+ publicizeConnRefreshClick = function( event ) {
+ event.preventDefault();
+ var popupURL = event.currentTarget.href;
+ var popupTitle = event.currentTarget.title;
+ // open a popup window
+ // when it is closed, kick off the tests again
+ var popupWin = window.open( popupURL, popupTitle, '' );
+ var popupWinTimer= window.setInterval( function() {
+ if ( popupWin.closed !== false ) {
+ window.clearInterval( popupWinTimer );
+ publicizeConnTestStart();
+ }
+ }, 500 );
+ }
+
+ publicizeConnTestComplete = function( response ) {
+ var testsSelector = $( '#pub-connection-tests' );
+ testsSelector
+ .removeClass( 'test-in-progress' )
+ .removeClass( 'below-h2' )
+ .removeClass( 'error' )
+ .removeClass( 'publicize-token-refresh-message' )
+ .html( '' );
+
+ // If any of the tests failed, show some stuff
+ var somethingShownAlready = false;
+ var facebookNotice = false;
+ $.each( response.data, function( index, testResult ) {
+ // find the li for this connection
+ if ( ! testResult.connectionTestPassed && testResult.userCanRefresh ) {
+ if ( ! somethingShownAlready ) {
+ testsSelector
+ .addClass( 'below-h2' )
+ .addClass( 'error' )
+ .addClass( 'publicize-token-refresh-message' )
+ .append( "<p><?php echo esc_html( __( 'Before you hit Publish, please refresh the following connection(s) to make sure we can Publicize your post:', 'jetpack' ) ); ?></p>" );
+ somethingShownAlready = true;
+ }
+
+ if ( testResult.userCanRefresh ) {
+ testsSelector.append( '<p/>' );
+ $( '<a/>', {
+ 'class' : 'pub-refresh-button button',
+ 'title' : testResult.refreshText,
+ 'href' : testResult.refreshURL,
+ 'text' : testResult.refreshText,
+ 'target' : '_refresh_' + testResult.serviceName
+ } )
+ .appendTo( testsSelector.children().last() )
+ .click( publicizeConnRefreshClick );
+ }
+ }
+
+ if( ! testResult.connectionTestPassed && ! testResult.userCanRefresh ) {
+ $( '#wpas-submit-' + testResult.unique_id ).prop( "checked", false ).prop( "disabled", true );
+ if ( ! facebookNotice ) {
+ var message = '<p>'
+ + testResult.connectionTestMessage
+ + '</p><p>'
+ + ' <a class="button" href="<?php echo esc_url( $this->publicize_settings_url ); ?>" rel="noopener noreferrer" target="_blank">'
+ + '<?php echo esc_html( __( 'Update Your Sharing Settings' ,'jetpack' ) ); ?>'
+ + '</a>'
+ + '<p>';
+
+ testsSelector
+ .addClass( 'below-h2' )
+ .addClass( 'error' )
+ .addClass( 'publicize-token-refresh-message' )
+ .append( message );
+ facebookNotice = true;
+ }
+ }
+ } );
+ }
+
+ $( document ).ready( function() {
+ // If we have the #pub-connection-tests div present, kick off the connection test
+ if ( $( '#pub-connection-tests' ).length ) {
+ publicizeConnTestStart();
+ }
+ } );
+
+} );
+</script>
+
+<style type="text/css">
+#publicize {
+ line-height: 1.5;
+}
+#publicize ul {
+ margin: 4px 0 4px 6px;
+}
+#publicize li {
+ margin: 0;
+}
+#publicize textarea {
+ margin: 4px 0 0;
+ width: 100%
+}
+#publicize ul.not-connected {
+ list-style: square;
+ padding-left: 1em;
+}
+.publicize__notice-warning {
+ display: block;
+ padding: 7px 10px;
+ margin: 5px 0;
+ border-left-width: 4px;
+ border-left-style: solid;
+ font-size: 12px;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+}
+.publicize-external-link {
+ display: block;
+ text-decoration: none;
+ margin-top: 8px;
+}
+.publicize-external-link__text {
+ text-decoration: underline;
+}
+#publicize-title:before {
+ content: "\f237";
+ font: normal 20px/1 dashicons;
+ speak: none;
+ margin-left: -1px;
+ padding-right: 3px;
+ vertical-align: top;
+ -webkit-font-smoothing: antialiased;
+ color: #82878c;
+}
+.post-new-php .authorize-link, .post-php .authorize-link {
+ line-height: 1.5em;
+}
+.post-new-php .authorize-message, .post-php .authorize-message {
+ margin-bottom: 0;
+}
+#poststuff #publicize .updated p {
+ margin: .5em 0;
+}
+.wpas-twitter-length-limit {
+ color: red;
+}
+.publicize__notice-warning .dashicons {
+ font-size: 16px;
+ text-decoration: none;
+}
+</style><?php
+ }
+
+ /**
+ * @param string $service_label Service's human-readable Label ("Facebook", "Twitter", ...)
+ * @param string $display_name Connection's human-readable Username ("@jetpack", ...)
+ * @return string
+ */
+ private function connection_label( $service_label, $display_name ) {
+ return sprintf(
+ /* translators: %1$s: Service Name (Facebook, Twitter, ...), %2$s: Username on Service (@jetpack, ...) */
+ __( '%1$s: %2$s', 'jetpack' ),
+ $service_label,
+ $display_name
+ );
+ }
+
+ /**
+ * Extracts the connections that require reauthentication, for example, LinkedIn, when it switched v1 to v2 of its API.
+ *
+ * @return array Connections that must be reauthenticated
+ */
+ function get_must_reauth_connections() {
+ $must_reauth = array();
+ $connections = $this->publicize->get_connections( 'linkedin' );
+ if ( is_array( $connections ) ) {
+ foreach ( $connections as $index => $connection ) {
+ if ( $this->publicize->is_invalid_linkedin_connection( $connection ) ) {
+ $must_reauth[ $index ] = 'LinkedIn';
+ }
+ }
+ }
+ return $must_reauth;
+ }
+
+ /**
+ * Controls the metabox that is displayed on the post page
+ * Allows the user to customize the message that will be sent out to the social network, as well as pick which
+ * networks to publish to. Also displays the character counter and some other information.
+ */
+ function post_page_metabox() {
+ global $post;
+
+ if ( ! $this->publicize->post_type_is_publicizeable( $post->post_type ) )
+ return;
+
+ $user_id = empty( $post->post_author ) ? $GLOBALS['user_ID'] : $post->post_author;
+ $connections_data = $this->publicize->get_filtered_connection_data();
+
+ $available_services = $this->publicize->get_services( 'all' );
+
+ if ( ! is_array( $available_services ) )
+ $available_services = array();
+
+ if ( ! is_array( $connections_data ) )
+ $connections_data = array();
+ ?>
+ <div id="publicize" class="misc-pub-section misc-pub-section-last">
+ <span id="publicize-title">
+ <?php
+ esc_html_e( 'Publicize:', 'jetpack' );
+
+ if ( 0 < count( $connections_data ) ) :
+ $publicize_form = $this->get_metabox_form_connected( $connections_data );
+
+ $must_reauth = $this->get_must_reauth_connections();
+ if ( ! empty( $must_reauth ) ) {
+ foreach ( $must_reauth as $connection_name ) {
+ ?>
+ <span class="notice-warning publicize__notice-warning">
+ <?php
+ /* translators: %s is the name of a Pubilicize service like "LinkedIn" */
+ printf( esc_html__(
+ 'Your %s connection needs to be reauthenticated to continue working – head to Sharing to take care of it.',
+ 'jetpack'
+ ), $connection_name );
+ ?>
+ <a
+ class="publicize-external-link"
+ href="<?php echo publicize_calypso_url() ?>"
+ target="_blank"
+ >
+ <span class="publicize-external-link__text"><?php esc_html_e( 'Go to Sharing settings', 'jetpack' ); ?></span>
+ <span class="dashicons dashicons-external"></span>
+ </a>
+ </span>
+ <?php
+ }
+ ?>
+ <?php
+ }
+
+ $labels = array();
+ foreach ( $connections_data as $connection_data ) {
+ if ( ! $connection_data['enabled'] ) {
+ continue;
+ }
+
+ $labels[] = sprintf(
+ '<strong>%s</strong>',
+ esc_html( $this->connection_label( $connection_data['service_label'], $connection_data['display_name'] ) )
+ );
+ }
+
+ ?>
+ <span id="publicize-defaults"><?php echo join( ', ', $labels ); ?></span>
+ <a href="#" id="publicize-form-edit"><?php esc_html_e( 'Edit', 'jetpack' ); ?></a>&nbsp;<a href="<?php echo esc_url( $this->publicize_settings_url ); ?>" rel="noopener noreferrer" target="_blank"><?php _e( 'Settings', 'jetpack' ); ?></a><br />
+ <?php
+
+ else :
+ $publicize_form = $this->get_metabox_form_disconnected( $available_services );
+
+ ?>
+ <strong><?php echo __( 'Not Connected', 'jetpack' ); ?></strong>
+ <a href="#" id="publicize-disconnected-form-show"><?php esc_html_e( 'Edit', 'jetpack' ); ?></a><br />
+ <?php
+
+ endif;
+ ?>
+ </span>
+ <?php
+ /**
+ * Filter the Publicize details form.
+ *
+ * @module publicize
+ *
+ * @since 2.0.0
+ *
+ * @param string $publicize_form Publicize Details form appearing above Publish button in the editor.
+ */
+ echo apply_filters( 'publicize_form', $publicize_form );
+ ?>
+ </div> <?php // #publicize
+ }
+
+ /**
+ * Generates HTML content for connections form.
+ *
+ * @since 6.7
+ *
+ * @global WP_Post $post The current post instance being published.
+ *
+ * @param array $connections_data
+ *
+ * @return array {
+ * Array of content for generating connection form.
+ *
+ * @type string HTML content of form
+ * @type array {
+ * Array of connection labels for active connections only.
+ *
+ * @type string Connection label string.
+ * }
+ * }
+ */
+ private function get_metabox_form_connected( $connections_data ) {
+ global $post;
+
+ $all_done = $this->publicize->post_is_done_sharing();
+ $all_connections_done = true;
+
+ ob_start();
+
+ ?>
+ <div id="publicize-form" class="hide-if-js">
+ <ul>
+ <?php
+
+ foreach ( $connections_data as $connection_data ) {
+ $all_connections_done = $all_connections_done && $connection_data['done'];
+ ?>
+
+ <li>
+ <label for="wpas-submit-<?php echo esc_attr( $connection_data['unique_id'] ); ?>">
+ <input
+ type="checkbox"
+ name="wpas[submit][<?php echo esc_attr( $connection_data['unique_id'] ); ?>]"
+ id="wpas-submit-<?php echo esc_attr( $connection_data['unique_id'] ); ?>"
+ class="wpas-submit-<?php echo esc_attr( $connection_data['service_name'] ); ?>"
+ value="1"
+ <?php
+ checked( true, $connection_data['enabled'] );
+ disabled( false, $connection_data['toggleable'] );
+ ?>
+ />
+ <?php if ( $connection_data['enabled'] && ! $connection_data['toggleable'] ) : // Need to submit a value to force a global connection to POST ?>
+ <input
+ type="hidden"
+ name="wpas[submit][<?php echo esc_attr( $connection_data['unique_id'] ); ?>]"
+ value="1"
+ />
+ <?php endif; ?>
+
+ <?php echo esc_html( $this->connection_label( $connection_data['service_label'], $connection_data['display_name'] ) ); ?>
+
+ </label>
+ </li>
+ <?php
+ }
+
+ $title = get_post_meta( $post->ID, $this->publicize->POST_MESS, true );
+ if ( ! $title ) {
+ $title = '';
+ }
+
+ $all_done = $all_done || $all_connections_done;
+
+ ?>
+
+ </ul>
+
+ <label for="wpas-title"><?php _e( 'Custom Message:', 'jetpack' ); ?></label>
+ <span id="wpas-title-counter" class="alignright hide-if-no-js">0</span>
+ <textarea name="wpas_title" id="wpas-title"<?php disabled( $all_done ); ?>><?php echo esc_textarea( $title ); ?></textarea>
+ <a href="#" class="hide-if-no-js button" id="publicize-form-hide"><?php esc_html_e( 'OK', 'jetpack' ); ?></a>
+ <input type="hidden" name="wpas[0]" value="1" />
+ </div>
+
+ <?php if ( ! $all_done ) : ?>
+ <div id="pub-connection-tests"></div>
+ <?php endif; ?>
+ <?php // #publicize-form
+
+ return ob_get_clean();
+ }
+
+ private function get_metabox_form_disconnected( $available_services ) {
+ ob_start();
+ ?><div id="publicize-form" class="hide-if-js">
+ <div id="add-publicize-check" style="display: none;"></div>
+
+ <?php _e( 'Connect to', 'jetpack' ); ?>:
+
+ <ul class="not-connected">
+ <?php foreach ( $available_services as $service_name => $service ) : ?>
+ <li>
+ <a class="pub-service" data-service="<?php echo esc_attr( $service_name ); ?>" title="<?php echo esc_attr( sprintf( __( 'Connect and share your posts on %s', 'jetpack' ), $this->publicize->get_service_label( $service_name ) ) ); ?>" rel="noopener noreferrer" target="_blank" href="<?php echo esc_url( $this->publicize->connect_url( $service_name ) ); ?>">
+ <?php echo esc_html( $this->publicize->get_service_label( $service_name ) ); ?>
+ </a>
+ </li>
+ <?php endforeach; ?>
+ </ul>
+ <a href="#" class="hide-if-no-js button" id="publicize-disconnected-form-hide"><?php esc_html_e( 'OK', 'jetpack' ); ?></a>
+ </div><?php // #publicize-form
+ return ob_get_clean();
+ }
+}
diff --git a/plugins/jetpack/modules/pwa.php b/plugins/jetpack/modules/pwa.php
new file mode 100644
index 00000000..b5e5e6a2
--- /dev/null
+++ b/plugins/jetpack/modules/pwa.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Module Name: Progressive Web Apps
+ * Module Description: Speed up and improve the reliability of your site using the latest in web technology.
+ * Sort Order: 38
+ * Recommendation Order: 18
+ * First Introduced: 5.6.0
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Developers
+ * Feature: Traffic
+ * Additional Search Queries: manifest, pwa, progressive
+ */
+
+require_once ( JETPACK__PLUGIN_DIR . 'modules/pwa/class.jetpack-pwa-helpers.php' );
+require_once ( JETPACK__PLUGIN_DIR . 'modules/pwa/class.jetpack-pwa-manifest.php' );
+
+class Jetpack_PWA {
+ /**
+ * @var Jetpack_PWA
+ */
+ private static $__instance = null;
+
+ /**
+ * Singleton implementation
+ *
+ * @return Jetpack_PWA
+ */
+ public static function instance() {
+ if ( is_null( self::$__instance ) ) {
+ self::$__instance = new Jetpack_PWA;
+ }
+
+ return self::$__instance;
+ }
+
+ private function __construct() {
+ Jetpack_PWA_Manifest::instance();
+ }
+}
+
+Jetpack_PWA::instance();
diff --git a/plugins/jetpack/modules/pwa/class.jetpack-pwa-helpers.php b/plugins/jetpack/modules/pwa/class.jetpack-pwa-helpers.php
new file mode 100644
index 00000000..4ac7602d
--- /dev/null
+++ b/plugins/jetpack/modules/pwa/class.jetpack-pwa-helpers.php
@@ -0,0 +1,62 @@
+<?php
+class Jetpack_PWA_Helpers {
+ public static function get_default_manifest_icon_sizes() {
+ // These icon sizes based on conversation here:
+ // https://github.com/GoogleChrome/lighthouse/issues/291
+ return array(
+ 192,
+ 512,
+ );
+ }
+
+ public static function site_icon_url( $size = 512 ) {
+ $url = get_site_icon_url( $size );
+
+ // Fall back to built-in WordPress icon
+ if ( ! $url && in_array( $size, self::get_default_manifest_icon_sizes() ) ) {
+ $url = esc_url_raw(
+ plugins_url( "modules/pwa/images/wp-$size.png", JETPACK__PLUGIN_FILE )
+ );
+ }
+
+ return $url;
+ }
+
+ public static function get_theme_color() {
+ $theme_color = false;
+
+ // if we have AMP enabled, use those colors?
+ if ( class_exists( 'AMP_Customizer_Settings' ) ) {
+ /* This filter is documented in wp-content/plugins/amp/includes/class-amp-post-template.php */
+ $amp_settings = apply_filters(
+ 'amp_post_template_customizer_settings',
+ AMP_Customizer_Settings::get_settings(),
+ null
+ );
+
+ if ( isset( $amp_settings['header_background_color'] ) ) {
+ $theme_color = $amp_settings['header_background_color'];
+ }
+ }
+
+ if ( ! $theme_color && current_theme_supports( 'custom-background' ) ) {
+ $background_color = get_background_color(); // Returns hex key without hash or empty string
+ if ( $background_color ) {
+ $theme_color = "#$background_color";
+ }
+ }
+
+ if ( ! $theme_color ) {
+ $theme_color = '#fff';
+ }
+
+ /**
+ * Allows overriding the PWA theme color which is used when loading the app.
+ *
+ * @since 5.6.0
+ *
+ * @param string $theme_color
+ */
+ return apply_filters( 'jetpack_pwa_background_color', $theme_color );
+ }
+}
diff --git a/plugins/jetpack/modules/pwa/class.jetpack-pwa-manifest.php b/plugins/jetpack/modules/pwa/class.jetpack-pwa-manifest.php
new file mode 100644
index 00000000..c294c2d0
--- /dev/null
+++ b/plugins/jetpack/modules/pwa/class.jetpack-pwa-manifest.php
@@ -0,0 +1,95 @@
+<?php
+
+class Jetpack_PWA_Manifest {
+ /**
+ * @var Jetpack_PWA_Manifest
+ */
+ private static $__instance = null;
+
+ /**
+ * When this query var is present, display the PWA manifest.
+ *
+ * @var string
+ */
+ const PWA_MANIFEST_QUERY_VAR = 'jetpack_app_manifest';
+
+ /**
+ * Singleton implementation
+ *
+ * @return Jetpack_PWA_Manifest
+ */
+ public static function instance() {
+ if ( is_null( self::$__instance ) ) {
+ self::$__instance = new Jetpack_PWA_Manifest;
+ }
+
+ return self::$__instance;
+ }
+
+ /**
+ * Registers actions the first time that instance() is called.
+ */
+ private function __construct() {
+ add_action( 'wp_head', array( $this, 'render_manifest_link' ) );
+ add_action( 'amp_post_template_head', array( $this, 'render_manifest_link' ) );
+ add_action( 'template_redirect', array( $this, 'render_manifest_json' ), 2 );
+ }
+
+ function render_manifest_link() {
+ ?>
+ <link rel="manifest" href="<?php echo esc_url_raw( $this->get_manifest_url() ); ?>">
+ <meta name="theme-color" content="<?php echo esc_attr( Jetpack_PWA_Helpers::get_theme_color() ); ?>">
+ <?php
+ }
+
+ public function get_manifest_url() {
+ return add_query_arg(
+ self::PWA_MANIFEST_QUERY_VAR, '1', home_url()
+ );
+ }
+
+ function render_manifest_json() {
+ // Do not load manifest in multiple locations
+ if ( is_front_page() && isset( $_GET[ self::PWA_MANIFEST_QUERY_VAR ] ) && $_GET[ self::PWA_MANIFEST_QUERY_VAR ] ) {
+ @ini_set( 'display_errors', false ); // Display errors can cause the XML to be not well formed.
+
+ $theme_color = Jetpack_PWA_Helpers::get_theme_color();
+
+ $manifest = array(
+ 'name' => get_bloginfo( 'name' ),
+ 'start_url' => get_home_url(),
+ 'short_name' => substr( get_bloginfo( 'name' ), 0, 12 ),
+ 'display' => 'standalone',
+ 'background_color' => $theme_color,
+ 'theme_color' => $theme_color,
+ );
+
+ if ( $description = get_bloginfo( 'description' ) ) {
+ $manifest['description'] = $description;
+ }
+
+ $manifest['icons'] = array_map(
+ array( $this, 'build_icon_object' ),
+ Jetpack_PWA_Helpers::get_default_manifest_icon_sizes()
+ );
+
+ /**
+ * Allow overriding the manifest.
+ *
+ * @since 5.6.0
+ *
+ * @param array $manifest
+ */
+ $manifest = apply_filters( 'jetpack_pwa_manifest', $manifest );
+
+ wp_send_json( $manifest );
+ }
+ }
+
+ function build_icon_object( $size ) {
+ return array(
+ 'src' => Jetpack_PWA_Helpers::site_icon_url( $size ),
+ 'sizes' => sprintf( '%1$dx%1$d', $size ),
+ );
+ }
+}
diff --git a/plugins/jetpack/modules/pwa/images/wp-192.png b/plugins/jetpack/modules/pwa/images/wp-192.png
new file mode 100644
index 00000000..9bfd50f7
--- /dev/null
+++ b/plugins/jetpack/modules/pwa/images/wp-192.png
Binary files differ
diff --git a/plugins/jetpack/modules/pwa/images/wp-512.png b/plugins/jetpack/modules/pwa/images/wp-512.png
new file mode 100644
index 00000000..36106626
--- /dev/null
+++ b/plugins/jetpack/modules/pwa/images/wp-512.png
Binary files differ
diff --git a/plugins/jetpack/modules/random-redirect.php b/plugins/jetpack/modules/random-redirect.php
new file mode 100644
index 00000000..88c13090
--- /dev/null
+++ b/plugins/jetpack/modules/random-redirect.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer needed.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/related-posts.php b/plugins/jetpack/modules/related-posts.php
new file mode 100644
index 00000000..a5387e38
--- /dev/null
+++ b/plugins/jetpack/modules/related-posts.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Module Name: Related posts
+ * Module Description: Keep visitors engaged on your blog by highlighting relevant and new content at the bottom of each published post.
+ * First Introduced: 2.9
+ * Sort Order: 29
+ * Recommendation Order: 9
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Recommended
+ * Feature: Engagement
+ * Additional Search Queries: related, jetpack related posts, related posts for wordpress, related posts, popular posts, popular, related content, related post, contextual, context, contextual related posts, related articles, similar posts, easy related posts, related page, simple related posts, free related posts, related thumbnails, similar, engagement, yet another related posts plugin
+ */
+class Jetpack_RelatedPosts_Module {
+ /**
+ * Class variables
+ */
+ private static $__instance = null;
+
+ /**
+ * Singleton implementation
+ *
+ * @return object
+ */
+ public static function instance() {
+ if ( ! is_a( self::$__instance, 'Jetpack_RelatedPosts_Module' ) )
+ self::$__instance = new Jetpack_RelatedPosts_Module();
+
+ return self::$__instance;
+ }
+
+ /**
+ * Register actions and filters
+ *
+ * @uses add_action, add_filter
+ */
+ private function __construct() {
+ add_action( 'jetpack_module_loaded_related-posts', array( $this, 'action_on_load' ) );
+ }
+
+ /**
+ * This action triggers if the module is in an active state, load related posts and options.
+ *
+ * @uses Jetpack_RelatedPosts::init, is_admin, Jetpack::enable_module_configurable, Jetpack_Sync::sync_posts
+ * @return null
+ */
+ public function action_on_load() {
+ require_once 'related-posts/jetpack-related-posts.php';
+ Jetpack_RelatedPosts::init();
+
+ if ( is_admin() ) {
+ Jetpack::enable_module_configurable( __FILE__ );
+ }
+
+ // Load Customizer controls.
+ if ( class_exists( 'WP_Customize_Manager' ) ) {
+ require_once 'related-posts/class.related-posts-customize.php';
+ }
+ }
+}
+
+// Do it.
+Jetpack_RelatedPosts_Module::instance();
diff --git a/plugins/jetpack/modules/related-posts/class.related-posts-customize.php b/plugins/jetpack/modules/related-posts/class.related-posts-customize.php
new file mode 100644
index 00000000..8f15ca69
--- /dev/null
+++ b/plugins/jetpack/modules/related-posts/class.related-posts-customize.php
@@ -0,0 +1,300 @@
+<?php
+
+// Exit if file is accessed directly
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class to include elements to modify Related Posts look in Customizer.
+ *
+ * @since 4.4.0
+ */
+class Jetpack_Related_Posts_Customize {
+
+ /**
+ * Key for panel, section and prefix for options. Same option name than in Options > Reading.
+ *
+ * @var string
+ */
+ var $prefix = 'jetpack_relatedposts';
+
+ /**
+ * @var string Control to focus when customizer loads.
+ */
+ var $focus = '';
+
+ /**
+ * Class initialization.
+ *
+ * @since 4.4.0
+ */
+ function __construct() {
+ add_action( 'customize_register', array( $this, 'customize_register' ) );
+ add_action( 'customize_controls_enqueue_scripts', array( $this, 'customize_controls_enqueue_scripts' ) );
+ }
+
+ /**
+ * Initialize Customizer controls.
+ *
+ * @since 4.4.0
+ *
+ * @param WP_Customize_Manager $wp_customize Customizer instance.
+ */
+ function customize_register( $wp_customize ) {
+
+ $wp_customize->add_section( $this->prefix,
+ array(
+ 'title' => esc_html__( 'Related Posts', 'jetpack' ),
+ 'description' => '',
+ 'capability' => 'edit_theme_options',
+ 'priority' => 200,
+ )
+ );
+
+ $selective_options = array();
+
+ foreach ( $this->get_options( $wp_customize ) as $key => $field ) {
+ $control_id = "$this->prefix[$key]";
+ $selective_options[] = $control_id;
+ $wp_customize->add_setting( $control_id,
+ array(
+ 'default' => isset( $field['default'] ) ? $field['default'] : '',
+ 'type' => isset( $field['setting_type'] ) ? $field['setting_type'] : 'option',
+ 'capability' => isset( $field['capability'] ) ? $field['capability'] : 'edit_theme_options',
+ 'transport' => isset( $field['transport'] ) ? $field['transport'] : 'postMessage',
+ )
+ );
+ $control_settings = array(
+ 'label' => isset( $field['label'] ) ? $field['label'] : '',
+ 'description' => isset( $field['description'] ) ? $field['description'] : '',
+ 'settings' => $control_id,
+ 'type' => isset( $field['control_type'] ) ? $field['control_type'] : 'text',
+ 'section' => $this->prefix,
+ 'priority' => 10,
+ 'active_callback' => isset( $field['active_callback'] ) ? $field['active_callback'] : __CLASS__ . '::is_single',
+ );
+ switch ( $field['control_type'] ) {
+ case 'text':
+ case 'checkbox':
+ default:
+ $wp_customize->add_control( new WP_Customize_Control( $wp_customize, $control_id, $control_settings ) );
+ break;
+ case 'select':
+ if ( isset( $field['choices'] ) ) {
+ $control_settings['choices'] = $field['choices'];
+ $wp_customize->add_control( new WP_Customize_Control( $wp_customize, $control_id, $control_settings ) );
+ }
+ break;
+ case 'message':
+ $wp_customize->add_control( new Jetpack_Message_Control( $wp_customize, $control_id, $control_settings ) );
+ break;
+ }
+ }
+
+ // If selective refresh is available, implement it.
+ if ( isset( $wp_customize->selective_refresh ) ) {
+ $wp_customize->selective_refresh->add_partial( "$this->prefix", array(
+ 'selector' => '.jp-relatedposts:not(.jp-relatedposts-block)',
+ 'settings' => $selective_options,
+ 'render_callback' => __CLASS__ . '::render_callback',
+ 'container_inclusive' => false,
+ ) );
+ }
+
+ }
+
+ /**
+ * Callback that outputs the headline based on user choice.
+ *
+ * @since 4.4.0
+ */
+ public static function render_callback() {
+ echo Jetpack_RelatedPosts::init()->get_headline();
+ }
+
+ /**
+ * Check whether the current post contains a Related Posts block.
+ *
+ * @since 6.9.0
+ *
+ * @return bool
+ */
+ public static function contains_related_posts_block() {
+ if ( has_block( 'jetpack/related-posts' ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check that we're in a single post view.
+ * Will return `false` if the current post contains a Related Posts block,
+ * because in that case we want to hide the Customizer controls.
+ *
+ * @since 4.4.0
+ *
+ * @return bool
+ */
+ public static function is_single() {
+ if ( self::contains_related_posts_block() ) {
+ return false;
+ }
+ return is_single();
+ }
+
+ /**
+ * Check that we're not in a single post view.
+ * Will return `false` if the current post contains a Related Posts block,
+ * because in that case we want to hide the Customizer controls.
+ *
+ * @since 4.4.0
+ *
+ * @return bool
+ */
+ public static function is_not_single() {
+ if ( self::contains_related_posts_block() ) {
+ return false;
+ }
+ return ! is_single();
+ }
+
+ /**
+ * Return list of options to modify.
+ *
+ * @since 4.4.0
+ *
+ * @param object $wp_customize Instance of WP Customizer
+ *
+ * @return mixed|void
+ */
+ function get_options( $wp_customize ) {
+ $transport = isset( $wp_customize->selective_refresh ) ? 'postMessage' : 'refresh';
+
+ $switched_locale = switch_to_locale( get_user_locale() );
+ $headline = __( 'Related', 'jetpack' );
+ if ( $switched_locale ) {
+ restore_previous_locale();
+ }
+
+ /**
+ * The filter allows you to change the options used to display Related Posts in the Customizer.
+ *
+ * @module related-posts
+ *
+ * @since 4.4.0
+ *
+ * @param array $options Array of options used to display Related Posts in the Customizer.
+ */
+ return apply_filters(
+ 'jetpack_related_posts_customize_options', array(
+ 'enabled' => array(
+ 'control_type' => 'hidden',
+ 'default' => 1,
+ 'setting_type' => 'option',
+ 'transport' => $transport,
+ ),
+ 'show_headline' => array(
+ 'label' => esc_html__( 'Show a headline', 'jetpack' ),
+ 'description' => esc_html__( 'This helps to clearly separate the related posts from post content.', 'jetpack' ),
+ 'control_type' => 'checkbox',
+ 'default' => 1,
+ 'setting_type' => 'option',
+ 'transport' => $transport,
+ ),
+ 'headline' => array(
+ 'label' => '',
+ 'description' => esc_html__( 'Enter text to use as headline.', 'jetpack' ),
+ 'control_type' => 'text',
+ 'default' => esc_html( $headline ),
+ 'setting_type' => 'option',
+ 'transport' => $transport,
+ ),
+ 'show_thumbnails' => array(
+ 'label' => esc_html__( 'Show thumbnails', 'jetpack' ),
+ 'description' => esc_html__( 'Show a thumbnail image where available.', 'jetpack' ),
+ 'control_type' => 'checkbox',
+ 'default' => 1,
+ 'setting_type' => 'option',
+ 'transport' => $transport,
+ ),
+ 'show_date' => array(
+ 'label' => esc_html__( 'Show date', 'jetpack' ),
+ 'description' => esc_html__( 'Display date when entry was published.', 'jetpack' ),
+ 'control_type' => 'checkbox',
+ 'default' => 1,
+ 'setting_type' => 'option',
+ 'transport' => $transport,
+ ),
+ 'show_context' => array(
+ 'label' => esc_html__( 'Show context', 'jetpack' ),
+ 'description' => esc_html__( "Display entry's category or tag.", 'jetpack' ),
+ 'control_type' => 'checkbox',
+ 'default' => 1,
+ 'setting_type' => 'option',
+ 'transport' => $transport,
+ ),
+ 'layout' => array(
+ 'label' => esc_html__( 'Layout', 'jetpack' ),
+ 'description' => esc_html__( 'Arrange entries in different layouts.', 'jetpack' ),
+ 'control_type' => 'select',
+ 'choices' => array(
+ 'grid' => esc_html__( 'Grid', 'jetpack' ),
+ 'list' => esc_html__( 'List', 'jetpack' ),
+ ),
+ 'default' => 'grid',
+ 'setting_type' => 'option',
+ 'transport' => $transport,
+ ),
+ 'msg_go_to_single' => array(
+ 'description' => esc_html__( 'Please visit a single post view to reveal the customization options.', 'jetpack' ),
+ 'control_type' => 'message',
+ 'active_callback' => __CLASS__ . '::is_not_single',
+ ),
+ 'msg_example' => array(
+ 'description' => esc_html__( 'Please note that the related posts displayed now are only for previewing purposes.', 'jetpack' ),
+ 'control_type' => 'message',
+ ),
+ )
+ );
+ }
+
+ /**
+ * Enqueue assets for Customizer controls.
+ *
+ * @since 4.4.0
+ */
+ function customize_controls_enqueue_scripts() {
+ wp_enqueue_script(
+ 'jetpack_related-posts-customizer',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/related-posts/related-posts-customizer.min.js',
+ 'modules/related-posts/related-posts-customizer.js'
+ ),
+ array( 'customize-controls' ),
+ JETPACK__VERSION
+ );
+ }
+
+} // class end
+
+/**
+ * Control that displays a message in Customizer.
+ *
+ * @since 4.4.0
+ */
+class Jetpack_Message_Control extends WP_Customize_Control {
+
+ /**
+ * Render the message.
+ *
+ * @since 4.4.0
+ */
+ public function render_content() {
+ echo '<p class="description">' . esc_html( $this->description ) . '</p>';
+ }
+} // class end
+
+// Initialize controls
+new Jetpack_Related_Posts_Customize;
diff --git a/plugins/jetpack/modules/related-posts/jetpack-related-posts.php b/plugins/jetpack/modules/related-posts/jetpack-related-posts.php
new file mode 100644
index 00000000..d1d4855f
--- /dev/null
+++ b/plugins/jetpack/modules/related-posts/jetpack-related-posts.php
@@ -0,0 +1,1804 @@
+<?php
+class Jetpack_RelatedPosts {
+ const VERSION = '20190204';
+ const SHORTCODE = 'jetpack-related-posts';
+
+ private static $instance = null;
+ private static $instance_raw = null;
+
+ /**
+ * Creates and returns a static instance of Jetpack_RelatedPosts.
+ *
+ * @return Jetpack_RelatedPosts
+ */
+ public static function init() {
+ if ( ! self::$instance ) {
+ if ( class_exists('WPCOM_RelatedPosts') && method_exists( 'WPCOM_RelatedPosts', 'init' ) ) {
+ self::$instance = WPCOM_RelatedPosts::init();
+ } else {
+ self::$instance = new Jetpack_RelatedPosts();
+ }
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Creates and returns a static instance of Jetpack_RelatedPosts_Raw.
+ *
+ * @return Jetpack_RelatedPosts
+ */
+ public static function init_raw() {
+ if ( ! self::$instance_raw ) {
+ if ( class_exists('WPCOM_RelatedPosts') && method_exists( 'WPCOM_RelatedPosts', 'init_raw' ) ) {
+ self::$instance_raw = WPCOM_RelatedPosts::init_raw();
+ } else {
+ self::$instance_raw = new Jetpack_RelatedPosts_Raw();
+ }
+ }
+
+ return self::$instance_raw;
+ }
+
+ protected $_options;
+ protected $_allow_feature_toggle;
+ protected $_blog_charset;
+ protected $_convert_charset;
+ protected $_previous_post_id;
+ protected $_found_shortcode = false;
+
+ /**
+ * Constructor for Jetpack_RelatedPosts.
+ *
+ * @uses get_option, add_action, apply_filters
+ * @return null
+ */
+ public function __construct() {
+ $this->_blog_charset = get_option( 'blog_charset' );
+ $this->_convert_charset = ( function_exists( 'iconv' ) && ! preg_match( '/^utf\-?8$/i', $this->_blog_charset ) );
+
+ add_action( 'admin_init', array( $this, 'action_admin_init' ) );
+ add_action( 'wp', array( $this, 'action_frontend_init' ) );
+
+ if ( ! class_exists( 'Jetpack_Media_Summary' ) ) {
+ jetpack_require_lib( 'class.media-summary' );
+ }
+
+ // Add Related Posts to the REST API Post response.
+ add_action( 'rest_api_init', array( $this, 'rest_register_related_posts' ) );
+
+ jetpack_register_block(
+ 'jetpack/related-posts',
+ array(
+ 'render_callback' => array( $this, 'render_block' ),
+ )
+ );
+ }
+
+ protected function get_blog_id() {
+ return Jetpack_Options::get_option( 'id' );
+ }
+
+ /**
+ * =================
+ * ACTIONS & FILTERS
+ * =================
+ */
+
+ /**
+ * Add a checkbox field to Settings > Reading for enabling related posts.
+ *
+ * @action admin_init
+ * @uses add_settings_field, __, register_setting, add_action
+ * @return null
+ */
+ public function action_admin_init() {
+
+ // Add the setting field [jetpack_relatedposts] and place it in Settings > Reading
+ add_settings_field( 'jetpack_relatedposts', '<span id="jetpack_relatedposts">' . __( 'Related posts', 'jetpack' ) . '</span>', array( $this, 'print_setting_html' ), 'reading' );
+ register_setting( 'reading', 'jetpack_relatedposts', array( $this, 'parse_options' ) );
+ add_action('admin_head', array( $this, 'print_setting_head' ) );
+
+ if( 'options-reading.php' == $GLOBALS['pagenow'] ) {
+ // Enqueue style for live preview on the reading settings page
+ $this->_enqueue_assets( false, true );
+ }
+ }
+
+ /**
+ * Load related posts assets if it's a elegiable front end page or execute search and return JSON if it's an endpoint request.
+ *
+ * @global $_GET
+ * @action wp
+ * @uses add_shortcode, get_the_ID
+ * @returns null
+ */
+ public function action_frontend_init() {
+ // Add a shortcode handler that outputs nothing, this gets overridden later if we can display related content
+ add_shortcode( self::SHORTCODE, array( $this, 'get_target_html_unsupported' ) );
+
+ if ( ! $this->_enabled_for_request() )
+ return;
+
+ if ( isset( $_GET['relatedposts'] ) ) {
+ $excludes = $this->parse_numeric_get_arg( 'relatedposts_exclude' );
+ $this->_action_frontend_init_ajax( $excludes );
+ } else {
+ if ( isset( $_GET['relatedposts_hit'], $_GET['relatedposts_origin'], $_GET['relatedposts_position'] ) ) {
+ $this->_log_click( $_GET['relatedposts_origin'], get_the_ID(), $_GET['relatedposts_position'] );
+ $this->_previous_post_id = (int) $_GET['relatedposts_origin'];
+ }
+
+ $this->_action_frontend_init_page();
+ }
+
+ }
+
+ /**
+ * Render insertion point.
+ *
+ * @since 4.2.0
+ *
+ * @return string
+ */
+ public function get_headline() {
+ $options = $this->get_options();
+
+ if ( $options['show_headline'] ) {
+ $headline = sprintf(
+ /** This filter is already documented in modules/sharedaddy/sharing-service.php */
+ apply_filters( 'jetpack_sharing_headline_html', '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>', esc_html( $options['headline'] ), 'related-posts' ),
+ esc_html( $options['headline'] )
+ );
+ } else {
+ $headline = '';
+ }
+ return $headline;
+ }
+
+ /**
+ * Adds a target to the post content to load related posts into if a shortcode for it did not already exist.
+ * Will skip adding the target if the post content contains a Related Posts block.
+ *
+ * @filter the_content
+ * @param string $content
+ * @returns string
+ */
+ public function filter_add_target_to_dom( $content ) {
+ if ( has_block( 'jetpack/related-posts', $content ) ) {
+ return $content;
+ }
+
+ if ( ! $this->_found_shortcode ) {
+ $content .= "\n" . $this->get_target_html();
+ }
+
+ return $content;
+ }
+
+ /**
+ * Looks for our shortcode on the unfiltered content, this has to execute early.
+ *
+ * @filter the_content
+ * @param string $content
+ * @uses has_shortcode
+ * @returns string
+ */
+ public function test_for_shortcode( $content ) {
+ $this->_found_shortcode = has_shortcode( $content, self::SHORTCODE );
+
+ return $content;
+ }
+
+ /**
+ * Returns the HTML for the related posts section.
+ *
+ * @uses esc_html__, apply_filters
+ * @returns string
+ */
+ public function get_target_html() {
+ require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
+ if ( Jetpack_Sync_Settings::is_syncing() ) {
+ return '';
+ }
+
+ /**
+ * Filter the Related Posts headline.
+ *
+ * @module related-posts
+ *
+ * @since 3.0.0
+ *
+ * @param string $headline Related Posts heading.
+ */
+ $headline = apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() );
+
+ if ( $this->_previous_post_id ) {
+ $exclude = "data-exclude='{$this->_previous_post_id}'";
+ } else {
+ $exclude = "";
+ }
+
+ return <<<EOT
+<div id='jp-relatedposts' class='jp-relatedposts' $exclude>
+ $headline
+</div>
+EOT;
+ }
+
+ /**
+ * Returns the HTML for the related posts section if it's running in the loop or other instances where we don't support related posts.
+ *
+ * @returns string
+ */
+ public function get_target_html_unsupported() {
+ require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
+ if ( Jetpack_Sync_Settings::is_syncing() ) {
+ return '';
+ }
+ return "\n\n<!-- Jetpack Related Posts is not supported in this context. -->\n\n";
+ }
+
+ /**
+ * ===============
+ * GUTENBERG BLOCK
+ * ===============
+ */
+
+ /**
+ * Echoes out items for the Gutenberg block
+ *
+ * @param array $related_post The post oject.
+ * @param array $block_attributes The block attributes.
+ */
+ public function render_block_item( $related_post, $block_attributes ) {
+ $instance_id = 'related-posts-item-' . uniqid();
+ $label_id = $instance_id . '-label';
+
+ $item_markup = sprintf(
+ '<ul id="%1$s" aria-labelledby="%2$s" class="jp-related-posts-i2__post" role="menuitem">',
+ esc_attr( $instance_id ),
+ esc_attr( $label_id )
+ );
+
+ $item_markup .= sprintf(
+ '<li class="jp-related-posts-i2__post-link"><a id="%1$s" href="%2$s" rel="%4$s">%3$s</a></li>',
+ esc_attr( $label_id ),
+ esc_url( $related_post['url'] ),
+ esc_attr( $related_post['title'] ),
+ esc_attr( $related_post['rel'] )
+ );
+
+ if ( ! empty( $block_attributes['show_thumbnails'] ) && ! empty( $related_post['img']['src'] ) ) {
+ $img_link = sprintf(
+ '<li class="jp-related-posts-i2__post-img-link"><a href="%1$s" rel="%2$s"><img src="%3$s" width="%4$s" alt="%5$s" /></a></li>',
+ esc_url( $related_post['url'] ),
+ esc_attr( $related_post['rel'] ),
+ esc_url( $related_post['img']['src'] ),
+ esc_attr( $related_post['img']['width'] ),
+ esc_attr( $related_post['img']['alt_text'] )
+ );
+
+ $item_markup .= $img_link;
+ }
+
+ if ( $block_attributes['show_date'] ) {
+ $date_tag = sprintf(
+ '<li class="jp-related-posts-i2__post-date">%1$s</li>',
+ esc_html( $related_post['date'] )
+ );
+
+ $item_markup .= $date_tag;
+ }
+
+ if ( ( $block_attributes['show_context'] ) && ! empty( $related_post['context'] ) ) {
+ $context_tag = sprintf(
+ '<li class="jp-related-posts-i2__post-context">%1$s</li>',
+ esc_html( $related_post['context'] )
+ );
+
+ $item_markup .= $context_tag;
+ }
+
+ $item_markup .= '</ul>';
+
+ return $item_markup;
+ }
+
+ /**
+ * Render a related posts row.
+ *
+ * @param array $posts The posts to render into the row.
+ * @param array $block_attributes Block attributes.
+ */
+ public function render_block_row( $posts, $block_attributes ) {
+ $rows_markup = '';
+ foreach ( $posts as $post ) {
+ $rows_markup .= $this->render_block_item( $post, $block_attributes );
+ }
+ return sprintf(
+ '<div class="jp-related-posts-i2__row" data-post-count="%1$s">%2$s</div>',
+ count( $posts ),
+ $rows_markup
+ );
+ }
+
+ /**
+ * Render the related posts markup.
+ *
+ * @param array $attributes Block attributes.
+ * @return string
+ */
+ public function render_block( $attributes ) {
+ $block_attributes = array(
+ 'show_thumbnails' => isset( $attributes['displayThumbnails'] ) && $attributes['displayThumbnails'],
+ 'show_date' => isset( $attributes['displayDate'] ) ? (bool) $attributes['displayDate'] : true,
+ 'show_context' => isset( $attributes['displayContext'] ) && $attributes['displayContext'],
+ 'layout' => isset( $attributes['postLayout'] ) && 'list' === $attributes['postLayout'] ? $attributes['postLayout'] : 'grid',
+ 'size' => ! empty( $attributes['postsToShow'] ) ? absint( $attributes['postsToShow'] ) : 3,
+ );
+
+ $excludes = $this->parse_numeric_get_arg( 'relatedposts_origin' );
+ $related_posts = $this->get_for_post_id(
+ get_the_ID(),
+ array(
+ 'size' => $block_attributes['size'],
+ 'exclude_post_ids' => $excludes,
+ )
+ );
+
+ $display_lower_row = $block_attributes['size'] > 3;
+
+ if ( empty( $related_posts ) ) {
+ return '';
+ }
+
+ switch ( count( $related_posts ) ) {
+ case 2:
+ case 4:
+ case 5:
+ $top_row_end = 2;
+ break;
+
+ default:
+ $top_row_end = 3;
+ break;
+ }
+
+ $upper_row_posts = array_slice( $related_posts, 0, $top_row_end );
+ $lower_row_posts = array_slice( $related_posts, $top_row_end );
+
+ $rows_markup = $this->render_block_row( $upper_row_posts, $block_attributes );
+ if ( $display_lower_row ) {
+ $rows_markup .= $this->render_block_row( $lower_row_posts, $block_attributes );
+ }
+
+ $target_to_dom_priority = has_filter(
+ 'the_content',
+ array( $this, 'filter_add_target_to_dom' )
+ );
+ remove_filter(
+ 'the_content',
+ array( $this, 'filter_add_target_to_dom' ),
+ $target_to_dom_priority
+ );
+
+ /*
+ Below is a hack to get the block content to render correctly.
+
+ This functionality should be covered in /inc/blocks.php but due to an error,
+ this has not been fixed as of this writing.
+
+ Alda has submitted a patch to Core in order to have this issue fixed at
+ https://core.trac.wordpress.org/attachment/ticket/45495/do_blocks.diff and
+ hopefully it makes to to the final RC of WP 5.1.
+ */
+ $priority = has_filter( 'the_content', 'wpautop' );
+ remove_filter( 'the_content', 'wpautop', $priority );
+ add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 );
+
+ return sprintf(
+ '<nav class="jp-relatedposts-i2" data-layout="%1$s">%2$s</nav>',
+ esc_attr( $block_attributes['layout'] ),
+ $rows_markup
+ );
+ }
+
+ /**
+ * ========================
+ * PUBLIC UTILITY FUNCTIONS
+ * ========================
+ */
+
+ /**
+ * Parse a numeric GET variable to an array of values.
+ *
+ * @since 6.9.0
+ *
+ * @uses absint
+ *
+ * @param string $arg Name of the GET variable
+ * @return array $result Parsed value(s)
+ */
+ public function parse_numeric_get_arg( $arg ) {
+ $result = array();
+
+ if ( isset( $_GET[ $arg ] ) ) {
+ if ( is_string( $_GET[ $arg ] ) ) {
+ $result = explode( ',', $_GET[ $arg ] );
+ } elseif ( is_array( $_GET[ $arg ] ) ) {
+ $result = array_values( $_GET[ $arg ] );
+ }
+
+ $result = array_unique( array_filter( array_map( 'absint', $result ) ) );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Gets options set for Jetpack_RelatedPosts and merge with defaults.
+ *
+ * @uses Jetpack_Options::get_option, apply_filters
+ * @return array
+ */
+ public function get_options() {
+ if ( null === $this->_options ) {
+ $this->_options = Jetpack_Options::get_option( 'relatedposts', array() );
+ if ( ! is_array( $this->_options ) )
+ $this->_options = array();
+ if ( ! isset( $this->_options['enabled'] ) )
+ $this->_options['enabled'] = true;
+ if ( ! isset( $this->_options['show_headline'] ) )
+ $this->_options['show_headline'] = true;
+ if ( ! isset( $this->_options['show_thumbnails'] ) )
+ $this->_options['show_thumbnails'] = false;
+ if ( ! isset( $this->_options['show_date'] ) ) {
+ $this->_options['show_date'] = true;
+ }
+ if ( ! isset( $this->_options['show_context'] ) ) {
+ $this->_options['show_context'] = true;
+ }
+ if ( ! isset( $this->_options['layout'] ) ) {
+ $this->_options['layout'] = 'grid';
+ }
+ if ( ! isset( $this->_options['headline'] ) ) {
+ $this->_options['headline'] = esc_html__( 'Related', 'jetpack' );
+ }
+ if ( empty( $this->_options['size'] ) || (int)$this->_options['size'] < 1 )
+ $this->_options['size'] = 3;
+
+ /**
+ * Filter Related Posts basic options.
+ *
+ * @module related-posts
+ *
+ * @since 2.8.0
+ *
+ * @param array $this->_options Array of basic Related Posts options.
+ */
+ $this->_options = apply_filters( 'jetpack_relatedposts_filter_options', $this->_options );
+ }
+
+ return $this->_options;
+ }
+
+ public function get_option( $option_name ) {
+ $options = $this->get_options();
+
+ if ( isset( $options[ $option_name ] ) ) {
+ return $options[ $option_name ];
+ }
+
+ return false;
+ }
+
+ /**
+ * Parses input and returns normalized options array.
+ *
+ * @param array $input
+ * @uses self::get_options
+ * @return array
+ */
+ public function parse_options( $input ) {
+ $current = $this->get_options();
+
+ if ( !is_array( $input ) )
+ $input = array();
+
+ if (
+ ! isset( $input['enabled'] )
+ || isset( $input['show_date'] )
+ || isset( $input['show_context'] )
+ || isset( $input['layout'] )
+ || isset( $input['headline'] )
+ ) {
+ $input['enabled'] = '1';
+ }
+
+ if ( '1' == $input['enabled'] ) {
+ $current['enabled'] = true;
+ $current['show_headline'] = ( isset( $input['show_headline'] ) && '1' == $input['show_headline'] );
+ $current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] );
+ $current['show_date'] = ( isset( $input['show_date'] ) && '1' == $input['show_date'] );
+ $current['show_context'] = ( isset( $input['show_context'] ) && '1' == $input['show_context'] );
+ $current['layout'] = isset( $input['layout'] ) && in_array( $input['layout'], array( 'grid', 'list' ), true ) ? $input['layout'] : 'grid';
+ $current['headline'] = isset( $input['headline'] ) ? $input['headline'] : esc_html__( 'Related', 'jetpack' );
+ } else {
+ $current['enabled'] = false;
+ }
+
+ if ( isset( $input['size'] ) && (int)$input['size'] > 0 )
+ $current['size'] = (int)$input['size'];
+ else
+ $current['size'] = null;
+
+ return $current;
+ }
+
+ /**
+ * HTML for admin settings page.
+ *
+ * @uses self::get_options, checked, esc_html__
+ * @returns null
+ */
+ public function print_setting_html() {
+ $options = $this->get_options();
+
+ $ui_settings_template = <<<EOT
+<p class="description">%s</p>
+<ul id="settings-reading-relatedposts-customize">
+ <li>
+ <label><input name="jetpack_relatedposts[show_headline]" type="checkbox" value="1" %s /> %s</label>
+ </li>
+ <li>
+ <label><input name="jetpack_relatedposts[show_thumbnails]" type="checkbox" value="1" %s /> %s</label>
+ </li>
+ <li>
+ <label><input name="jetpack_relatedposts[show_date]" type="checkbox" value="1" %s /> %s</label>
+ </li>
+ <li>
+ <label><input name="jetpack_relatedposts[show_context]" type="checkbox" value="1" %s /> %s</label>
+ </li>
+</ul>
+<div id='settings-reading-relatedposts-preview'>
+ %s
+ <div id="jp-relatedposts" class="jp-relatedposts"></div>
+</div>
+EOT;
+ $ui_settings = sprintf(
+ $ui_settings_template,
+ esc_html__( 'The following settings will impact all related posts on your site, except for those you created via the block editor:', 'jetpack' ),
+ checked( $options['show_headline'], true, false ),
+ esc_html__( 'Highlight related content with a heading', 'jetpack' ),
+ checked( $options['show_thumbnails'], true, false ),
+ esc_html__( 'Show a thumbnail image where available', 'jetpack' ),
+ checked( $options['show_date'], true, false ),
+ esc_html__( 'Show entry date', 'jetpack' ),
+ checked( $options['show_context'], true, false ),
+ esc_html__( 'Show context (category or tag)', 'jetpack' ),
+ esc_html__( 'Preview:', 'jetpack' )
+ );
+
+ if ( !$this->_allow_feature_toggle() ) {
+ $template = <<<EOT
+<input type="hidden" name="jetpack_relatedposts[enabled]" value="1" />
+%s
+EOT;
+ printf(
+ $template,
+ $ui_settings
+ );
+ } else {
+ $template = <<<EOT
+<ul id="settings-reading-relatedposts">
+ <li>
+ <label><input type="radio" name="jetpack_relatedposts[enabled]" value="0" class="tog" %s /> %s</label>
+ </li>
+ <li>
+ <label><input type="radio" name="jetpack_relatedposts[enabled]" value="1" class="tog" %s /> %s</label>
+ %s
+ </li>
+</ul>
+EOT;
+ printf(
+ $template,
+ checked( $options['enabled'], false, false ),
+ esc_html__( 'Hide related content after posts', 'jetpack' ),
+ checked( $options['enabled'], true, false ),
+ esc_html__( 'Show related content after posts', 'jetpack' ),
+ $ui_settings
+ );
+ }
+ }
+
+ /**
+ * Head JS/CSS for admin settings page.
+ *
+ * @uses esc_html__
+ * @returns null
+ */
+ public function print_setting_head() {
+
+ // only dislay the Related Posts JavaScript on the Reading Settings Admin Page
+ $current_screen = get_current_screen();
+
+ if ( is_null( $current_screen ) ) {
+ return;
+ }
+
+ if( 'options-reading' != $current_screen->id )
+ return;
+
+ $related_headline = sprintf(
+ '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>',
+ esc_html__( 'Related', 'jetpack' )
+ );
+
+ $href_params = 'class="jp-relatedposts-post-a" href="#jetpack_relatedposts" rel="nofollow" data-origin="0" data-position="0"';
+ $related_with_images = <<<EOT
+<div class="jp-relatedposts-items jp-relatedposts-items-visual">
+ <div class="jp-relatedposts-post jp-relatedposts-post0 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
+ <a $href_params>
+ <img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2019/03/cat-blog.png" width="350" alt="Big iPhone/iPad Update Now Available" scale="0">
+ </a>
+ <h4 class="jp-relatedposts-post-title">
+ <a $href_params>Big iPhone/iPad Update Now Available</a>
+ </h4>
+ <p class="jp-relatedposts-post-excerpt">Big iPhone/iPad Update Now Available</p>
+ <p class="jp-relatedposts-post-context">In "Mobile"</p>
+ </div>
+ <div class="jp-relatedposts-post jp-relatedposts-post1 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
+ <a $href_params>
+ <img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2019/03/devices.jpg" width="350" alt="The WordPress for Android App Gets a Big Facelift" scale="0">
+ </a>
+ <h4 class="jp-relatedposts-post-title">
+ <a $href_params>The WordPress for Android App Gets a Big Facelift</a>
+ </h4>
+ <p class="jp-relatedposts-post-excerpt">The WordPress for Android App Gets a Big Facelift</p>
+ <p class="jp-relatedposts-post-context">In "Mobile"</p>
+ </div>
+ <div class="jp-relatedposts-post jp-relatedposts-post2 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
+ <a $href_params>
+ <img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg" width="350" alt="Upgrade Focus: VideoPress For Weddings" scale="0">
+ </a>
+ <h4 class="jp-relatedposts-post-title">
+ <a $href_params>Upgrade Focus: VideoPress For Weddings</a>
+ </h4>
+ <p class="jp-relatedposts-post-excerpt">Upgrade Focus: VideoPress For Weddings</p>
+ <p class="jp-relatedposts-post-context">In "Upgrade"</p>
+ </div>
+</div>
+EOT;
+ $related_with_images = str_replace( "\n", '', $related_with_images );
+ $related_without_images = <<<EOT
+<div class="jp-relatedposts-items jp-relatedposts-items-minimal">
+ <p class="jp-relatedposts-post jp-relatedposts-post0" data-post-id="0" data-post-format="image">
+ <span class="jp-relatedposts-post-title"><a $href_params>Big iPhone/iPad Update Now Available</a></span>
+ <span class="jp-relatedposts-post-context">In "Mobile"</span>
+ </p>
+ <p class="jp-relatedposts-post jp-relatedposts-post1" data-post-id="0" data-post-format="image">
+ <span class="jp-relatedposts-post-title"><a $href_params>The WordPress for Android App Gets a Big Facelift</a></span>
+ <span class="jp-relatedposts-post-context">In "Mobile"</span>
+ </p>
+ <p class="jp-relatedposts-post jp-relatedposts-post2" data-post-id="0" data-post-format="image">
+ <span class="jp-relatedposts-post-title"><a $href_params>Upgrade Focus: VideoPress For Weddings</a></span>
+ <span class="jp-relatedposts-post-context">In "Upgrade"</span>
+ </p>
+</div>
+EOT;
+ $related_without_images = str_replace( "\n", '', $related_without_images );
+
+ if ( $this->_allow_feature_toggle() ) {
+ $extra_css = '#settings-reading-relatedposts-customize { padding-left:2em; margin-top:.5em; }';
+ } else {
+ $extra_css = '';
+ }
+
+ echo <<<EOT
+<style type="text/css">
+ #settings-reading-relatedposts .disabled { opacity:.5; filter:Alpha(opacity=50); }
+ #settings-reading-relatedposts-preview .jp-relatedposts { background:#fff; padding:.5em; width:75%; }
+ $extra_css
+</style>
+<script type="text/javascript">
+ jQuery( document ).ready( function($) {
+ var update_ui = function() {
+ var is_enabled = true;
+ if ( 'radio' == $( 'input[name="jetpack_relatedposts[enabled]"]' ).attr('type') ) {
+ if ( '0' == $( 'input[name="jetpack_relatedposts[enabled]"]:checked' ).val() ) {
+ is_enabled = false;
+ }
+ }
+ if ( is_enabled ) {
+ $( '#settings-reading-relatedposts-customize' )
+ .removeClass( 'disabled' )
+ .find( 'input' )
+ .attr( 'disabled', false );
+ $( '#settings-reading-relatedposts-preview' )
+ .removeClass( 'disabled' );
+ } else {
+ $( '#settings-reading-relatedposts-customize' )
+ .addClass( 'disabled' )
+ .find( 'input' )
+ .attr( 'disabled', true );
+ $( '#settings-reading-relatedposts-preview' )
+ .addClass( 'disabled' );
+ }
+ };
+
+ var update_preview = function() {
+ var html = '';
+ if ( $( 'input[name="jetpack_relatedposts[show_headline]"]:checked' ).length ) {
+ html += '$related_headline';
+ }
+ if ( $( 'input[name="jetpack_relatedposts[show_thumbnails]"]:checked' ).length ) {
+ html += '$related_with_images';
+ } else {
+ html += '$related_without_images';
+ }
+ $( '#settings-reading-relatedposts-preview .jp-relatedposts' ).html( html );
+ if ( $( 'input[name="jetpack_relatedposts[show_date]"]:checked' ).length ) {
+ $( '.jp-relatedposts-post-title' ).each( function() {
+ $( this ).after( $( '<span>August 8, 2005</span>' ) );
+ } );
+ }
+ if ( $( 'input[name="jetpack_relatedposts[show_context]"]:checked' ).length ) {
+ $( '.jp-relatedposts-post-context' ).show();
+ } else {
+ $( '.jp-relatedposts-post-context' ).hide();
+ }
+ $( '#settings-reading-relatedposts-preview .jp-relatedposts' ).show();
+ };
+
+ // Update on load
+ update_preview();
+ update_ui();
+
+ // Update on change
+ $( '#settings-reading-relatedposts-customize input' )
+ .change( update_preview );
+ $( '#settings-reading-relatedposts' )
+ .find( 'input.tog' )
+ .change( update_ui );
+ });
+</script>
+EOT;
+ }
+
+ /**
+ * Gets an array of related posts that match the given post_id.
+ *
+ * @param int $post_id Post which we want to find related posts for.
+ * @param array $args - params to use when building Elasticsearch filters to narrow down the search domain.
+ * @uses self::get_options, get_post_type, wp_parse_args, apply_filters
+ * @return array
+ */
+ public function get_for_post_id( $post_id, array $args ) {
+ $options = $this->get_options();
+
+ if ( ! empty( $args['size'] ) ) {
+ $options['size'] = $args['size'];
+ }
+
+ if (
+ ! $options['enabled']
+ || 0 === (int) $post_id
+ || empty( $options['size'] )
+ ) {
+ return array();
+ }
+
+ $defaults = array(
+ 'size' => (int) $options['size'],
+ 'post_type' => get_post_type( $post_id ),
+ 'post_formats' => array(),
+ 'has_terms' => array(),
+ 'date_range' => array(),
+ 'exclude_post_ids' => array(),
+ );
+ $args = wp_parse_args( $args, $defaults );
+ /**
+ * Filter the arguments used to retrieve a list of Related Posts.
+ *
+ * @module related-posts
+ *
+ * @since 2.8.0
+ *
+ * @param array $args Array of options to retrieve Related Posts.
+ * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
+ */
+ $args = apply_filters( 'jetpack_relatedposts_filter_args', $args, $post_id );
+
+ $filters = $this->_get_es_filters_from_args( $post_id, $args );
+ /**
+ * Filter Elasticsearch options used to calculate Related Posts.
+ *
+ * @module related-posts
+ *
+ * @since 2.8.0
+ *
+ * @param array $filters Array of Elasticsearch filters based on the post_id and args.
+ * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
+ */
+ $filters = apply_filters( 'jetpack_relatedposts_filter_filters', $filters, $post_id );
+
+ $results = $this->_get_related_posts( $post_id, $args['size'], $filters );
+ /**
+ * Filter the array of related posts matched by Elasticsearch.
+ *
+ * @module related-posts
+ *
+ * @since 2.8.0
+ *
+ * @param array $results Array of related posts matched by Elasticsearch.
+ * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
+ */
+ return apply_filters( 'jetpack_relatedposts_returned_results', $results, $post_id );
+ }
+
+ /**
+ * =========================
+ * PRIVATE UTILITY FUNCTIONS
+ * =========================
+ */
+
+ /**
+ * Creates an array of Elasticsearch filters based on the post_id and args.
+ *
+ * @param int $post_id
+ * @param array $args
+ * @uses apply_filters, get_post_types, get_post_format_strings
+ * @return array
+ */
+ protected function _get_es_filters_from_args( $post_id, array $args ) {
+ $filters = array();
+
+ /**
+ * Filter the terms used to search for Related Posts.
+ *
+ * @module related-posts
+ *
+ * @since 2.8.0
+ *
+ * @param array $args['has_terms'] Array of terms associated to the Related Posts.
+ * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
+ */
+ $args['has_terms'] = apply_filters( 'jetpack_relatedposts_filter_has_terms', $args['has_terms'], $post_id );
+ if ( ! empty( $args['has_terms'] ) ) {
+ foreach( (array)$args['has_terms'] as $term ) {
+ if ( mb_strlen( $term->taxonomy ) ) {
+ switch ( $term->taxonomy ) {
+ case 'post_tag':
+ $tax_fld = 'tag.slug';
+ break;
+ case 'category':
+ $tax_fld = 'category.slug';
+ break;
+ default:
+ $tax_fld = 'taxonomy.' . $term->taxonomy . '.slug';
+ break;
+ }
+ $filters[] = array( 'term' => array( $tax_fld => $term->slug ) );
+ }
+ }
+ }
+
+ /**
+ * Filter the Post Types where we search Related Posts.
+ *
+ * @module related-posts
+ *
+ * @since 2.8.0
+ *
+ * @param array $args['post_type'] Array of Post Types.
+ * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
+ */
+ $args['post_type'] = apply_filters( 'jetpack_relatedposts_filter_post_type', $args['post_type'], $post_id );
+ $valid_post_types = get_post_types();
+ if ( is_array( $args['post_type'] ) ) {
+ $sanitized_post_types = array();
+ foreach ( $args['post_type'] as $pt ) {
+ if ( in_array( $pt, $valid_post_types ) )
+ $sanitized_post_types[] = $pt;
+ }
+ if ( ! empty( $sanitized_post_types ) )
+ $filters[] = array( 'terms' => array( 'post_type' => $sanitized_post_types ) );
+ } else if ( in_array( $args['post_type'], $valid_post_types ) && 'all' != $args['post_type'] ) {
+ $filters[] = array( 'term' => array( 'post_type' => $args['post_type'] ) );
+ }
+
+ /**
+ * Filter the Post Formats where we search Related Posts.
+ *
+ * @module related-posts
+ *
+ * @since 3.3.0
+ *
+ * @param array $args['post_formats'] Array of Post Formats.
+ * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
+ */
+ $args['post_formats'] = apply_filters( 'jetpack_relatedposts_filter_post_formats', $args['post_formats'], $post_id );
+ $valid_post_formats = get_post_format_strings();
+ $sanitized_post_formats = array();
+ foreach ( $args['post_formats'] as $pf ) {
+ if ( array_key_exists( $pf, $valid_post_formats ) ) {
+ $sanitized_post_formats[] = $pf;
+ }
+ }
+ if ( ! empty( $sanitized_post_formats ) ) {
+ $filters[] = array( 'terms' => array( 'post_format' => $sanitized_post_formats ) );
+ }
+
+ /**
+ * Filter the date range used to search Related Posts.
+ *
+ * @module related-posts
+ *
+ * @since 2.8.0
+ *
+ * @param array $args['date_range'] Array of a month interval where we search Related Posts.
+ * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
+ */
+ $args['date_range'] = apply_filters( 'jetpack_relatedposts_filter_date_range', $args['date_range'], $post_id );
+ if ( is_array( $args['date_range'] ) && ! empty( $args['date_range'] ) ) {
+ $args['date_range'] = array_map( 'intval', $args['date_range'] );
+ if ( !empty( $args['date_range']['from'] ) && !empty( $args['date_range']['to'] ) ) {
+ $filters[] = array(
+ 'range' => array(
+ 'date_gmt' => $this->_get_coalesced_range( $args['date_range'] ),
+ )
+ );
+ }
+ }
+
+ /**
+ * Filter the Post IDs excluded from appearing in Related Posts.
+ *
+ * @module related-posts
+ *
+ * @since 2.9.0
+ *
+ * @param array $args['exclude_post_ids'] Array of Post IDs.
+ * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
+ */
+ $args['exclude_post_ids'] = apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', $args['exclude_post_ids'], $post_id );
+ if ( !empty( $args['exclude_post_ids'] ) && is_array( $args['exclude_post_ids'] ) ) {
+ $excluded_post_ids = array();
+ foreach ( $args['exclude_post_ids'] as $exclude_post_id) {
+ $exclude_post_id = (int)$exclude_post_id;
+ if ( $exclude_post_id > 0 )
+ $excluded_post_ids[] = $exclude_post_id;
+ }
+ $filters[] = array( 'not' => array( 'terms' => array( 'post_id' => $excluded_post_ids ) ) );
+ }
+
+ return $filters;
+ }
+
+ /**
+ * Takes a range and coalesces it into a month interval bracketed by a time as determined by the blog_id to enhance caching.
+ *
+ * @param array $date_range
+ * @return array
+ */
+ protected function _get_coalesced_range( array $date_range ) {
+ $now = time();
+ $coalesce_time = $this->get_blog_id() % 86400;
+ $current_time = $now - strtotime( 'today', $now );
+
+ if ( $current_time < $coalesce_time && '01' == date( 'd', $now ) ) {
+ // Move back 1 period
+ return array(
+ 'from' => date( 'Y-m-01', strtotime( '-1 month', $date_range['from'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),
+ 'to' => date( 'Y-m-01', $date_range['to'] ) . ' ' . date( 'H:i:s', $coalesce_time ),
+ );
+ } else {
+ // Use current period
+ return array(
+ 'from' => date( 'Y-m-01', $date_range['from'] ) . ' ' . date( 'H:i:s', $coalesce_time ),
+ 'to' => date( 'Y-m-01', strtotime( '+1 month', $date_range['to'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),
+ );
+ }
+ }
+
+ /**
+ * Generate and output ajax response for related posts API call.
+ * NOTE: Calls exit() to end all further processing after payload has been outputed.
+ *
+ * @param array $excludes array of post_ids to exclude
+ * @uses send_nosniff_header, self::get_for_post_id, get_the_ID
+ * @return null
+ */
+ protected function _action_frontend_init_ajax( array $excludes ) {
+ define( 'DOING_AJAX', true );
+
+ header( 'Content-type: application/json; charset=utf-8' ); // JSON can only be UTF-8
+ send_nosniff_header();
+
+ $options = $this->get_options();
+
+ if ( isset( $_GET['jetpackrpcustomize'] ) ) {
+
+ // If we're in the customizer, add dummy content.
+ $date_now = current_time( get_option( 'date_format' ) );
+ $related_posts = array(
+ array(
+ 'id' => - 1,
+ 'url' => 'https://jetpackme.files.wordpress.com/2019/03/cat-blog.png',
+ 'url_meta' => array(
+ 'origin' => 0,
+ 'position' => 0
+ ),
+ 'title' => esc_html__( 'Big iPhone/iPad Update Now Available', 'jetpack' ),
+ 'date' => $date_now,
+ 'format' => false,
+ 'excerpt' => esc_html__( 'It is that time of the year when devices are shiny again.', 'jetpack' ),
+ 'rel' => 'nofollow',
+ 'context' => esc_html__( 'In "Mobile"', 'jetpack' ),
+ 'img' => array(
+ 'src' => 'https://jetpackme.files.wordpress.com/2019/03/cat-blog.png',
+ 'width' => 350,
+ 'height' => 200
+ ),
+ 'classes' => array()
+ ),
+ array(
+ 'id' => - 1,
+ 'url' => 'https://jetpackme.files.wordpress.com/2019/03/devices.jpg',
+ 'url_meta' => array(
+ 'origin' => 0,
+ 'position' => 0
+ ),
+ 'title' => esc_html__( 'The WordPress for Android App Gets a Big Facelift', 'jetpack' ),
+ 'date' => $date_now,
+ 'format' => false,
+ 'excerpt' => esc_html__( 'Writing is new again in Android with the new WordPress app.', 'jetpack' ),
+ 'rel' => 'nofollow',
+ 'context' => esc_html__( 'In "Mobile"', 'jetpack' ),
+ 'img' => array(
+ 'src' => 'https://jetpackme.files.wordpress.com/2019/03/devices.jpg',
+ 'width' => 350,
+ 'height' => 200
+ ),
+ 'classes' => array()
+ ),
+ array(
+ 'id' => - 1,
+ 'url' => 'https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg',
+ 'url_meta' => array(
+ 'origin' => 0,
+ 'position' => 0
+ ),
+ 'title' => esc_html__( 'Upgrade Focus, VideoPress for weddings', 'jetpack' ),
+ 'date' => $date_now,
+ 'format' => false,
+ 'excerpt' => esc_html__( 'Weddings are in the spotlight now with VideoPress for weddings.', 'jetpack' ),
+ 'rel' => 'nofollow',
+ 'context' => esc_html__( 'In "Mobile"', 'jetpack' ),
+ 'img' => array(
+ 'src' => 'https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg',
+ 'width' => 350,
+ 'height' => 200
+ ),
+ 'classes' => array()
+ ),
+ );
+
+ for ( $total = 0; $total < $options['size'] - 3; $total++ ) {
+ $related_posts[] = $related_posts[ $total ];
+ }
+
+ $current_post = get_post();
+
+ // Exclude current post after filtering to make sure it's excluded and not lost during filtering.
+ $excluded_posts = array_merge(
+ /** This filter is already documented in modules/related-posts/jetpack-related-posts.php */
+ apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', array() ),
+ array( $current_post->ID )
+ );
+
+ // Fetch posts with featured image.
+ $with_post_thumbnails = get_posts( array(
+ 'posts_per_page' => $options['size'],
+ 'post__not_in' => $excluded_posts,
+ 'post_type' => $current_post->post_type,
+ 'meta_key' => '_thumbnail_id',
+ 'suppress_filters' => false,
+ ) );
+
+ // If we don't have enough, fetch posts without featured image.
+ if ( 0 < ( $more = $options['size'] - count( $with_post_thumbnails ) ) ) {
+ $no_post_thumbnails = get_posts( array(
+ 'posts_per_page' => $more,
+ 'post__not_in' => $excluded_posts,
+ 'post_type' => $current_post->post_type,
+ 'meta_query' => array(
+ array(
+ 'key' => '_thumbnail_id',
+ 'compare' => 'NOT EXISTS',
+ ),
+ ),
+ 'suppress_filters' => false,
+ ) );
+ } else {
+ $no_post_thumbnails = array();
+ }
+
+ foreach ( array_merge( $with_post_thumbnails, $no_post_thumbnails ) as $index => $real_post ) {
+ $related_posts[ $index ]['id'] = $real_post->ID;
+ $related_posts[ $index ]['url'] = esc_url( get_permalink( $real_post ) );
+ $related_posts[ $index ]['title'] = $this->_to_utf8( $this->_get_title( $real_post->post_title, $real_post->post_content ) );
+ $related_posts[ $index ]['date'] = get_the_date( '', $real_post );
+ $related_posts[ $index ]['excerpt'] = html_entity_decode( $this->_to_utf8( $this->_get_excerpt( $real_post->post_excerpt, $real_post->post_content ) ), ENT_QUOTES, 'UTF-8' );
+ $related_posts[ $index ]['img'] = $this->_generate_related_post_image_params( $real_post->ID );
+ $related_posts[ $index ]['context'] = $this->_generate_related_post_context( $real_post->ID );
+ }
+ } else {
+ $related_posts = $this->get_for_post_id(
+ get_the_ID(),
+ array(
+ 'exclude_post_ids' => $excludes,
+ )
+ );
+ }
+
+ $response = array(
+ 'version' => self::VERSION,
+ 'show_thumbnails' => (bool) $options['show_thumbnails'],
+ 'show_date' => (bool) $options['show_date'],
+ 'show_context' => (bool) $options['show_context'],
+ 'layout' => (string) $options['layout'],
+ 'headline' => (string) $options['headline'],
+ 'items' => array(),
+ );
+
+ if ( count( $related_posts ) == $options['size'] )
+ $response['items'] = $related_posts;
+
+ echo json_encode( $response );
+
+ exit();
+ }
+
+ /**
+ * Returns a UTF-8 encoded array of post information for the given post_id
+ *
+ * @param int $post_id
+ * @param int $position
+ * @param int $origin The post id that this is related to
+ * @uses get_post, get_permalink, remove_query_arg, get_post_format, apply_filters
+ * @return array
+ */
+ public function get_related_post_data_for_post( $post_id, $position, $origin ) {
+ $post = get_post( $post_id );
+
+ return array(
+ 'id' => $post->ID,
+ 'url' => get_permalink( $post->ID ),
+ 'url_meta' => array( 'origin' => $origin, 'position' => $position ),
+ 'title' => $this->_to_utf8( $this->_get_title( $post->post_title, $post->post_content ) ),
+ 'date' => get_the_date( '', $post->ID ),
+ 'format' => get_post_format( $post->ID ),
+ 'excerpt' => html_entity_decode( $this->_to_utf8( $this->_get_excerpt( $post->post_excerpt, $post->post_content ) ), ENT_QUOTES, 'UTF-8' ),
+ /**
+ * Filters the rel attribute for the Related Posts' links.
+ *
+ * @module related-posts
+ *
+ * @since 3.7.0
+ *
+ * @param string nofollow Link rel attribute for Related Posts' link. Default is nofollow.
+ * @param int $post->ID Post ID.
+ */
+ 'rel' => apply_filters( 'jetpack_relatedposts_filter_post_link_rel', 'nofollow', $post->ID ),
+ /**
+ * Filter the context displayed below each Related Post.
+ *
+ * @module related-posts
+ *
+ * @since 3.0.0
+ *
+ * @param string $this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ) Context displayed below each related post.
+ * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
+ */
+ 'context' => apply_filters(
+ 'jetpack_relatedposts_filter_post_context',
+ $this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ),
+ $post->ID
+ ),
+ 'img' => $this->_generate_related_post_image_params( $post->ID ),
+ /**
+ * Filter the post css classes added on HTML markup.
+ *
+ * @module related-posts
+ *
+ * @since 3.8.0
+ *
+ * @param array array() CSS classes added on post HTML markup.
+ * @param string $post_id Post ID.
+ */
+ 'classes' => apply_filters(
+ 'jetpack_relatedposts_filter_post_css_classes',
+ array(),
+ $post->ID
+ ),
+ );
+ }
+
+ /**
+ * Returns either the title or a small excerpt to use as title for post.
+ *
+ * @param string $post_title
+ * @param string $post_content
+ * @uses strip_shortcodes, wp_trim_words, __
+ * @return string
+ */
+ protected function _get_title( $post_title, $post_content ) {
+ if ( ! empty( $post_title ) ) {
+ return wp_strip_all_tags( $post_title );
+ }
+
+ $post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '…' );
+ if ( ! empty( $post_title ) ) {
+ return $post_title;
+ }
+
+ return __( 'Untitled Post', 'jetpack' );
+ }
+
+ /**
+ * Returns a plain text post excerpt for title attribute of links.
+ *
+ * @param string $post_excerpt
+ * @param string $post_content
+ * @uses strip_shortcodes, wp_strip_all_tags, wp_trim_words
+ * @return string
+ */
+ protected function _get_excerpt( $post_excerpt, $post_content ) {
+ if ( empty( $post_excerpt ) )
+ $excerpt = $post_content;
+ else
+ $excerpt = $post_excerpt;
+
+ return wp_trim_words( wp_strip_all_tags( strip_shortcodes( $excerpt ) ), 50, '…' );
+ }
+
+ /**
+ * Generates the thumbnail image to be used for the post. Uses the
+ * image as returned by Jetpack_PostImages::get_image()
+ *
+ * @param int $post_id
+ * @uses self::get_options, apply_filters, Jetpack_PostImages::get_image, Jetpack_PostImages::fit_image_url
+ * @return string
+ */
+ protected function _generate_related_post_image_params( $post_id ) {
+ $options = $this->get_options();
+ $image_params = array(
+ 'alt_text' => '',
+ 'src' => '',
+ 'width' => 0,
+ 'height' => 0,
+ );
+
+ /**
+ * Filter the size of the Related Posts images.
+ *
+ * @module related-posts
+ *
+ * @since 2.8.0
+ *
+ * @param array array( 'width' => 350, 'height' => 200 ) Size of the images displayed below each Related Post.
+ */
+ $thumbnail_size = apply_filters(
+ 'jetpack_relatedposts_filter_thumbnail_size',
+ array( 'width' => 350, 'height' => 200 )
+ );
+ if ( !is_array( $thumbnail_size ) ) {
+ $thumbnail_size = array(
+ 'width' => (int)$thumbnail_size,
+ 'height' => (int)$thumbnail_size
+ );
+ }
+
+ // Try to get post image
+ if ( class_exists( 'Jetpack_PostImages' ) ) {
+ $img_url = '';
+ $post_image = Jetpack_PostImages::get_image(
+ $post_id,
+ $thumbnail_size
+ );
+
+ if ( is_array($post_image) ) {
+ $img_url = $post_image['src'];
+ } elseif ( class_exists( 'Jetpack_Media_Summary' ) ) {
+ $media = Jetpack_Media_Summary::get( $post_id );
+
+ if ( is_array($media) && !empty( $media['image'] ) ) {
+ $img_url = $media['image'];
+ }
+ }
+
+ if ( ! empty( $img_url ) ) {
+ if ( ! empty( $post_image['alt_text'] ) ) {
+ $image_params['alt_text'] = $post_image['alt_text'];
+ } else {
+ $image_params['alt_text'] = '';
+ }
+ $image_params['width'] = $thumbnail_size['width'];
+ $image_params['height'] = $thumbnail_size['height'];
+ $image_params['src'] = Jetpack_PostImages::fit_image_url(
+ $img_url,
+ $thumbnail_size['width'],
+ $thumbnail_size['height']
+ );
+ }
+ }
+
+ return $image_params;
+ }
+
+ /**
+ * Returns the string UTF-8 encoded
+ *
+ * @param string $text
+ * @return string
+ */
+ protected function _to_utf8( $text ) {
+ if ( $this->_convert_charset ) {
+ return iconv( $this->_blog_charset, 'UTF-8', $text );
+ } else {
+ return $text;
+ }
+ }
+
+ /**
+ * =============================================
+ * PROTECTED UTILITY FUNCTIONS EXTENDED BY WPCOM
+ * =============================================
+ */
+
+ /**
+ * Workhorse method to return array of related posts matched by Elasticsearch.
+ *
+ * @param int $post_id
+ * @param int $size
+ * @param array $filters
+ * @uses wp_remote_post, is_wp_error, get_option, wp_remote_retrieve_body, get_post, add_query_arg, remove_query_arg, get_permalink, get_post_format, apply_filters
+ * @return array
+ */
+ protected function _get_related_posts( $post_id, $size, array $filters ) {
+ $hits = $this->_filter_non_public_posts(
+ $this->_get_related_post_ids(
+ $post_id,
+ $size,
+ $filters
+ )
+ );
+
+ /**
+ * Filter the Related Posts matched by Elasticsearch.
+ *
+ * @module related-posts
+ *
+ * @since 2.9.0
+ *
+ * @param array $hits Array of Post IDs matched by Elasticsearch.
+ * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
+ */
+ $hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
+
+ $related_posts = array();
+ foreach ( $hits as $i => $hit ) {
+ $related_posts[] = $this->get_related_post_data_for_post( $hit['id'], $i, $post_id );
+ }
+ return $related_posts;
+ }
+
+ /**
+ * Get array of related posts matched by Elasticsearch.
+ *
+ * @param int $post_id
+ * @param int $size
+ * @param array $filters
+ * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body, get_post_meta, update_post_meta
+ * @return array
+ */
+ protected function _get_related_post_ids( $post_id, $size, array $filters ) {
+ $now_ts = time();
+ $cache_meta_key = '_jetpack_related_posts_cache';
+
+ $body = array(
+ 'size' => (int) $size,
+ );
+
+ if ( !empty( $filters ) )
+ $body['filter'] = array( 'and' => $filters );
+
+ // Build cache key
+ $cache_key = md5( serialize( $body ) );
+
+ // Load all cached values
+ if ( wp_using_ext_object_cache() ) {
+ $transient_name = "{$cache_meta_key}_{$cache_key}_{$post_id}";
+ $cache = get_transient( $transient_name );
+ if ( false !== $cache ) {
+ return $cache;
+ }
+ } else {
+ $cache = get_post_meta( $post_id, $cache_meta_key, true );
+
+ if ( empty( $cache ) )
+ $cache = array();
+
+
+ // Cache is valid! Return cached value.
+ if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) && $cache[ $cache_key ][ 'expires' ] > $now_ts ) {
+ return $cache[ $cache_key ][ 'payload' ];
+ }
+ }
+
+ $response = wp_remote_post(
+ "https://public-api.wordpress.com/rest/v1/sites/{$this->get_blog_id()}/posts/$post_id/related/",
+ array(
+ 'timeout' => 10,
+ 'user-agent' => 'jetpack_related_posts',
+ 'sslverify' => true,
+ 'body' => $body,
+ )
+ );
+
+ // Oh no... return nothing don't cache errors.
+ if ( is_wp_error( $response ) ) {
+ if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) )
+ return $cache[ $cache_key ][ 'payload' ]; // return stale
+ else
+ return array();
+ }
+
+ $results = json_decode( wp_remote_retrieve_body( $response ), true );
+ $related_posts = array();
+ if ( is_array( $results ) && !empty( $results['hits'] ) ) {
+ foreach( $results['hits'] as $hit ) {
+ $related_posts[] = array(
+ 'id' => $hit['fields']['post_id'],
+ );
+ }
+ }
+
+ // An empty array might indicate no related posts or that posts
+ // are not yet synced to WordPress.com, so we cache for only 1
+ // minute in this case
+ if ( empty( $related_posts ) ) {
+ $cache_ttl = 60;
+ } else {
+ $cache_ttl = 12 * HOUR_IN_SECONDS;
+ }
+
+ // Update cache
+ if ( wp_using_ext_object_cache() ) {
+ set_transient( $transient_name, $related_posts, $cache_ttl );
+ } else {
+ // Copy all valid cache values
+ $new_cache = array();
+ foreach ( $cache as $k => $v ) {
+ if ( is_array( $v ) && $v[ 'expires' ] > $now_ts ) {
+ $new_cache[ $k ] = $v;
+ }
+ }
+
+ // Set new cache value
+ $cache_expires = $cache_ttl + $now_ts;
+ $new_cache[ $cache_key ] = array(
+ 'expires' => $cache_expires,
+ 'payload' => $related_posts,
+ );
+ update_post_meta( $post_id, $cache_meta_key, $new_cache );
+ }
+
+ return $related_posts;
+ }
+
+ /**
+ * Filter out any hits that are not public anymore.
+ *
+ * @param array $related_posts
+ * @uses get_post_stati, get_post_status
+ * @return array
+ */
+ protected function _filter_non_public_posts( array $related_posts ) {
+ $public_stati = get_post_stati( array( 'public' => true ) );
+
+ $filtered = array();
+ foreach ( $related_posts as $hit ) {
+ if ( in_array( get_post_status( $hit['id'] ), $public_stati ) ) {
+ $filtered[] = $hit;
+ }
+ }
+ return $filtered;
+ }
+
+ /**
+ * Generates a context for the related content (second line in related post output).
+ * Order of importance:
+ * - First category (Not 'Uncategorized')
+ * - First post tag
+ * - Number of comments
+ *
+ * @param int $post_id
+ * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n
+ * @return string
+ */
+ protected function _generate_related_post_context( $post_id ) {
+ $categories = get_the_category( $post_id );
+ if ( is_array( $categories ) ) {
+ foreach ( $categories as $category ) {
+ if ( 'uncategorized' != $category->slug && '' != trim( $category->name ) ) {
+ $post_cat_context = sprintf(
+ esc_html_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
+ $category->name
+ );
+ /**
+ * Filter the "In Category" line displayed in the post context below each Related Post.
+ *
+ * @module related-posts
+ *
+ * @since 3.2.0
+ *
+ * @param string $post_cat_context "In Category" line displayed in the post context below each Related Post.
+ * @param array $category Array containing information about the category.
+ */
+ return apply_filters( 'jetpack_relatedposts_post_category_context', $post_cat_context, $category );
+ }
+ }
+ }
+
+ $tags = get_the_terms( $post_id, 'post_tag' );
+ if ( is_array( $tags ) ) {
+ foreach ( $tags as $tag ) {
+ if ( '' != trim( $tag->name ) ) {
+ $post_tag_context = sprintf(
+ _x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
+ $tag->name
+ );
+ /**
+ * Filter the "In Tag" line displayed in the post context below each Related Post.
+ *
+ * @module related-posts
+ *
+ * @since 3.2.0
+ *
+ * @param string $post_tag_context "In Tag" line displayed in the post context below each Related Post.
+ * @param array $tag Array containing information about the tag.
+ */
+ return apply_filters( 'jetpack_relatedposts_post_tag_context', $post_tag_context, $tag );
+ }
+ }
+ }
+
+ $comment_count = get_comments_number( $post_id );
+ if ( $comment_count > 0 ) {
+ return sprintf(
+ _n( 'With 1 comment', 'With %s comments', $comment_count, 'jetpack' ),
+ number_format_i18n( $comment_count )
+ );
+ }
+
+ return __( 'Similar post', 'jetpack' );
+ }
+
+ /**
+ * Logs clicks for clickthrough analysis and related result tuning.
+ *
+ * @return null
+ */
+ protected function _log_click( $post_id, $to_post_id, $link_position ) {
+
+ }
+
+ /**
+ * Determines if the current post is able to use related posts.
+ *
+ * @uses self::get_options, is_admin, is_single, apply_filters
+ * @return bool
+ */
+ protected function _enabled_for_request() {
+ $enabled = is_single()
+ && ! is_admin()
+ && ( ! $this->_allow_feature_toggle() || $this->get_option( 'enabled' ) );
+
+ if (
+ class_exists( 'Jetpack_AMP_Support' )
+ && Jetpack_AMP_Support::is_amp_request()
+ ) {
+ $enabled = false;
+ }
+
+ /**
+ * Filter the Enabled value to allow related posts to be shown on pages as well.
+ *
+ * @module related-posts
+ *
+ * @since 3.3.0
+ *
+ * @param bool $enabled Should Related Posts be enabled on the current page.
+ */
+ return apply_filters( 'jetpack_relatedposts_filter_enabled_for_request', $enabled );
+ }
+
+ /**
+ * Adds filters and enqueues assets.
+ *
+ * @uses self::_enqueue_assets, self::_setup_shortcode, add_filter
+ * @return null
+ */
+ protected function _action_frontend_init_page() {
+ $this->_enqueue_assets( true, true );
+ $this->_setup_shortcode();
+
+ add_filter( 'the_content', array( $this, 'filter_add_target_to_dom' ), 40 );
+ }
+
+ /**
+ * Enqueues assets needed to do async loading of related posts.
+ *
+ * @uses wp_enqueue_script, wp_enqueue_style, plugins_url
+ * @return null
+ */
+ protected function _enqueue_assets( $script, $style ) {
+ $dependencies = is_customize_preview() ? array( 'customize-base' ) : array( 'jquery' );
+ if ( $script ) {
+ wp_enqueue_script(
+ 'jetpack_related-posts',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/related-posts/related-posts.min.js',
+ 'modules/related-posts/related-posts.js'
+ ),
+ $dependencies,
+ self::VERSION
+ );
+ $related_posts_js_options = array(
+ /**
+ * Filter each Related Post Heading structure.
+ *
+ * @since 4.0.0
+ *
+ * @param string $str Related Post Heading structure. Default to h4.
+ */
+ 'post_heading' => apply_filters( 'jetpack_relatedposts_filter_post_heading', esc_attr( 'h4' ) ),
+ );
+ wp_localize_script( 'jetpack_related-posts', 'related_posts_js_options', $related_posts_js_options );
+ }
+ if ( $style ){
+ wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'related-posts.css', __FILE__ ), array(), self::VERSION );
+ wp_style_add_data( 'jetpack_related-posts', 'rtl', 'replace' );
+ }
+ }
+
+ /**
+ * Sets up the shortcode processing.
+ *
+ * @uses add_filter, add_shortcode
+ * @return null
+ */
+ protected function _setup_shortcode() {
+ add_filter( 'the_content', array( $this, 'test_for_shortcode' ), 0 );
+
+ add_shortcode( self::SHORTCODE, array( $this, 'get_target_html' ) );
+ }
+
+ protected function _allow_feature_toggle() {
+ if ( null === $this->_allow_feature_toggle ) {
+ /**
+ * Filter the display of the Related Posts toggle in Settings > Reading.
+ *
+ * @module related-posts
+ *
+ * @since 2.8.0
+ *
+ * @param bool false Display a feature toggle. Default to false.
+ */
+ $this->_allow_feature_toggle = apply_filters( 'jetpack_relatedposts_filter_allow_feature_toggle', false );
+ }
+ return $this->_allow_feature_toggle;
+ }
+
+ /**
+ * ===================================================
+ * FUNCTIONS EXPOSING RELATED POSTS IN THE WP REST API
+ * ===================================================
+ */
+
+ /**
+ * Add Related Posts to the REST API Post response.
+ *
+ * @since 4.4.0
+ *
+ * @action rest_api_init
+ * @uses register_rest_field, self::rest_get_related_posts
+ * @return null
+ */
+ public function rest_register_related_posts() {
+ register_rest_field( 'post',
+ 'jetpack-related-posts',
+ array(
+ 'get_callback' => array( $this, 'rest_get_related_posts' ),
+ 'update_callback' => null,
+ 'schema' => null,
+ )
+ );
+ }
+
+ /**
+ * Build an array of Related Posts.
+ * By default returns cached results that are stored for up to 12 hours.
+ *
+ * @since 4.4.0
+ *
+ * @param array $object Details of current post.
+ * @param string $field_name Name of field.
+ * @param WP_REST_Request $request Current request
+ *
+ * @uses self::get_for_post_id
+ *
+ * @return array
+ */
+ public function rest_get_related_posts( $object, $field_name, $request ) {
+ return $this->get_for_post_id( $object['id'], array( 'size' => 6 ) );
+ }
+}
+
+class Jetpack_RelatedPosts_Raw extends Jetpack_RelatedPosts {
+ protected $_query_name;
+
+ /**
+ * Allows callers of this class to tag each query with a unique name for tracking purposes.
+ *
+ * @param string $name
+ * @return Jetpack_RelatedPosts_Raw
+ */
+ public function set_query_name( $name ) {
+ $this->_query_name = (string) $name;
+ return $this;
+ }
+
+ /**
+ * The raw related posts class can be used by other plugins or themes
+ * to get related content. This class wraps the existing RelatedPosts
+ * logic thus we never want to add anything to the DOM or do anything
+ * for event hooks. We will also not present any settings for this
+ * class and keep it enabled as calls to this class is done
+ * programmatically.
+ */
+ public function action_admin_init() {}
+ public function action_frontend_init() {}
+ public function get_options() {
+ return array(
+ 'enabled' => true,
+ );
+ }
+
+ /**
+ * Workhorse method to return array of related posts ids matched by Elasticsearch.
+ *
+ * @param int $post_id
+ * @param int $size
+ * @param array $filters
+ * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body
+ * @return array
+ */
+ protected function _get_related_posts( $post_id, $size, array $filters ) {
+ $hits = $this->_filter_non_public_posts(
+ $this->_get_related_post_ids(
+ $post_id,
+ $size,
+ $filters
+ )
+ );
+
+ /** This filter is already documented in modules/related-posts/related-posts.php */
+ $hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
+
+ return $hits;
+ }
+}
diff --git a/plugins/jetpack/modules/related-posts/related-posts-customizer.js b/plugins/jetpack/modules/related-posts/related-posts-customizer.js
new file mode 100644
index 00000000..b2d36e41
--- /dev/null
+++ b/plugins/jetpack/modules/related-posts/related-posts-customizer.js
@@ -0,0 +1,28 @@
+/**
+ * Adds functionality for Related Posts controls in Customizer.
+ */
+( function( api ) {
+ 'use strict';
+
+ api( 'jetpack_relatedposts[show_headline]', function( showHeadlineSetting ) {
+ var setupHeadlineControl = function( headlineControl ) {
+ var setActiveState, isDisplayed;
+
+ isDisplayed = function() {
+ return showHeadlineSetting.findControls()[ 0 ].active.get() && showHeadlineSetting.get();
+ };
+
+ setActiveState = function() {
+ headlineControl.active.set( isDisplayed() );
+ };
+
+ headlineControl.active.validate = isDisplayed;
+
+ setActiveState();
+
+ showHeadlineSetting.bind( setActiveState );
+ };
+
+ api.control( 'jetpack_relatedposts[headline]', setupHeadlineControl );
+ } );
+} )( wp.customize );
diff --git a/plugins/jetpack/modules/related-posts/related-posts-rtl.css b/plugins/jetpack/modules/related-posts/related-posts-rtl.css
new file mode 100644
index 00000000..21f37004
--- /dev/null
+++ b/plugins/jetpack/modules/related-posts/related-posts-rtl.css
@@ -0,0 +1 @@
+.jp-related-posts-i2__row{display:flex;margin-top:1.5rem}.jp-related-posts-i2__row:first-child{margin-top:0}.jp-related-posts-i2__post{flex-grow:1;flex-basis:0;margin:0 10px;display:flex;flex-direction:column;padding-right:0}.jp-related-posts-i2__row[data-post-count="3"] .jp-related-posts-i2__post{max-width:calc(33% - 20px)}.jp-related-posts-i2__row[data-post-count="1"] .jp-related-posts-i2__post,.jp-related-posts-i2__row[data-post-count="2"] .jp-related-posts-i2__post{max-width:calc(50% - 20px)}.jp-related-posts-i2__post-context,.jp-related-posts-i2__post-date,.jp-related-posts-i2__post-heading,.jp-related-posts-i2__post-img-link{flex-direction:row;display:block}.jp-related-posts-i2__post-heading{margin:.5rem 0;font-size:1rem;line-height:1.2em}.jp-related-posts-i2__post-link{display:block;width:100%;line-height:1.2em}.jp-related-posts-i2__post-img-link{order:-1;line-height:1em}.jp-related-posts-i2__post-img-link img{width:100%}.jp-relatedposts-i2[data-layout=list] .jp-related-posts-i2__row{margin-top:0;display:block}.jp-relatedposts-i2[data-layout=list] .jp-related-posts-i2__post{max-width:none;margin:0}.jp-relatedposts-i2[data-layout=list].jp-related-posts-i2__post-img-link{margin-top:1rem}@media only screen and (max-width:640px){.jp-related-posts-i2__row{margin-top:0;display:block}.jp-related-posts-i2__row[data-post-count] .jp-related-posts-i2__post{max-width:none;margin:0;margin-top:1rem}.jp-related-posts-i2__post-img-link{margin-top:1rem}.jp-related-posts-i2__post-img-link img{width:350px}}#jp-relatedposts{display:none;padding-top:1em;margin:1em 0;position:relative;clear:both}.jp-relatedposts:after{content:'';display:block;clear:both}#jp-relatedposts h3.jp-relatedposts-headline{margin:0 0 1em 0;display:inline-block;float:right;font-size:9pt;font-weight:700;font-family:inherit}#jp-relatedposts h3.jp-relatedposts-headline em:before{content:"";display:block;width:100%;min-width:30px;border-top:1px solid #ddd;border-top:1px solid rgba(0,0,0,.2);margin-bottom:1em}#jp-relatedposts h3.jp-relatedposts-headline em{font-style:normal;font-weight:700}#jp-relatedposts .jp-relatedposts-items{clear:right}#jp-relatedposts .jp-relatedposts-items-visual{margin-left:-20px}#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post{float:right;width:33%;margin:0 0 1em;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post{padding-left:20px;-moz-opacity:.8;opacity:.8}#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:nth-child(3n+4),#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post:nth-child(3n+4){clear:both}#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:hover .jp-relatedposts-post-title a{text-decoration:underline}#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:hover{-moz-opacity:1;opacity:1}#jp-relatedposts .jp-relatedposts-items p,#jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title{font-size:14px;line-height:20px;margin:0}#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post-nothumbs{position:relative}#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post-nothumbs a.jp-relatedposts-post-aoverlay{position:absolute;top:0;bottom:0;right:0;left:0;display:block;border-bottom:0}#jp-relatedposts .jp-relatedposts-items p{margin-bottom:0}#jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title{text-transform:none;margin:0;font-family:inherit;display:block;max-width:100%}#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a{font-size:inherit;font-weight:400;text-decoration:none;-moz-opacity:1;opacity:1}#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a:hover{text-decoration:underline}#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post img.jp-relatedposts-post-img,#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post span{display:block;max-width:90%;overflow:hidden;text-overflow:ellipsis}#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post img.jp-relatedposts-post-img,#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post span{max-width:100%}#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-context,#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-date{opacity:.6}.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-date{display:none}#jp-relatedposts .jp-relatedposts-items-visual div.jp-relatedposts-post-thumbs p.jp-relatedposts-post-excerpt{display:none}#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post-nothumbs p.jp-relatedposts-post-excerpt{overflow:hidden}#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post-nothumbs span{margin-bottom:1em}#jp-relatedposts .jp-relatedposts-list .jp-relatedposts-post{clear:both;width:100%}#jp-relatedposts .jp-relatedposts-list .jp-relatedposts-post img.jp-relatedposts-post-img{float:right;overflow:hidden;max-width:33%;margin-left:3%}#jp-relatedposts .jp-relatedposts-list h4.jp-relatedposts-post-title{display:inline-block;max-width:63%}@media only screen and (max-width:640px){#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post{width:50%}#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:nth-child(3n){clear:right}#jp-relatedposts .jp-relatedposts-items-visual{margin-left:20px}}@media only screen and (max-width:320px){#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post{width:100%;clear:both;margin:0 0 1em}#jp-relatedposts .jp-relatedposts-list .jp-relatedposts-post img.jp-relatedposts-post-img,#jp-relatedposts .jp-relatedposts-list h4.jp-relatedposts-post-title{float:none;max-width:100%;margin-left:0}} \ No newline at end of file
diff --git a/plugins/jetpack/modules/related-posts/related-posts.css b/plugins/jetpack/modules/related-posts/related-posts.css
new file mode 100644
index 00000000..763dfcdb
--- /dev/null
+++ b/plugins/jetpack/modules/related-posts/related-posts.css
@@ -0,0 +1,315 @@
+/**
+ * Jetpack related posts
+ */
+
+/**
+ * The Gutenberg block
+ */
+
+.jp-related-posts-i2__row {
+ display: flex;
+ margin-top: 1.5rem;
+}
+
+.jp-related-posts-i2__row:first-child {
+ margin-top: 0;
+}
+
+.jp-related-posts-i2__post {
+ flex-grow: 1;
+ flex-basis: 0;
+ margin: 0 10px;
+ display: flex;
+ flex-direction: column;
+ padding-left: 0;
+}
+
+.jp-related-posts-i2__row[data-post-count="3"] .jp-related-posts-i2__post {
+ max-width: calc(33% - 20px);
+}
+
+.jp-related-posts-i2__row[data-post-count="2"] .jp-related-posts-i2__post,
+.jp-related-posts-i2__row[data-post-count="1"] .jp-related-posts-i2__post {
+ max-width: calc(50% - 20px);
+}
+
+.jp-related-posts-i2__post-heading, .jp-related-posts-i2__post-img-link,
+.jp-related-posts-i2__post-date, .jp-related-posts-i2__post-context {
+ flex-direction: row;
+ display: block;
+}
+
+.jp-related-posts-i2__post-heading {
+ margin: 0.5rem 0;
+ font-size: 1rem;
+ line-height: 1.2em;
+}
+
+.jp-related-posts-i2__post-link {
+ display: block;
+ width: 100%;
+ line-height: 1.2em;
+}
+
+.jp-related-posts-i2__post-img-link {
+ order: -1;
+ line-height: 1em;
+}
+.jp-related-posts-i2__post-img-link img {
+ width: 100%;
+}
+
+/* List view */
+
+.jp-relatedposts-i2[data-layout="list"] .jp-related-posts-i2__row{
+ margin-top: 0;
+ display: block;
+}
+
+.jp-relatedposts-i2[data-layout="list"] .jp-related-posts-i2__post {
+ max-width: none;
+ margin: 0;
+}
+
+.jp-relatedposts-i2[data-layout="list"].jp-related-posts-i2__post-img-link {
+ margin-top: 1rem;
+}
+
+/* Breakpoints */
+@media only screen and (max-width: 640px) {
+ .jp-related-posts-i2__row {
+ margin-top: 0;
+ display: block;
+ }
+ .jp-related-posts-i2__row[data-post-count] .jp-related-posts-i2__post {
+ max-width: none;
+ margin: 0;
+ margin-top: 1rem;
+ }
+ .jp-related-posts-i2__post-img-link {
+ margin-top: 1rem;
+ }
+ .jp-related-posts-i2__post-img-link img {
+ width: 350px;
+ }
+}
+
+/* Container */
+
+#jp-relatedposts {
+ display: none;
+ padding-top: 1em;
+ margin: 1em 0;
+ position: relative;
+ clear: both;
+}
+
+.jp-relatedposts:after {
+ content: '';
+ display: block;
+ clear: both;
+}
+
+/* Headline above related posts section, labeled "Related" */
+
+#jp-relatedposts h3.jp-relatedposts-headline {
+ margin: 0 0 1em 0;
+ display: inline-block;
+ float: left;
+ font-size: 9pt;
+ font-weight: bold;
+ font-family: inherit;
+}
+
+#jp-relatedposts h3.jp-relatedposts-headline em:before {
+ content: "";
+ display: block;
+ width: 100%;
+ min-width: 30px;
+ border-top: 1px solid #ddd;
+ border-top: 1px solid rgba(0,0,0,.2);
+ margin-bottom: 1em;
+}
+
+#jp-relatedposts h3.jp-relatedposts-headline em {
+ font-style: normal;
+ font-weight: bold;
+}
+
+/* Related posts items (wrapping items) */
+
+#jp-relatedposts .jp-relatedposts-items {
+ clear: left;
+}
+
+#jp-relatedposts .jp-relatedposts-items-visual {
+ margin-right: -20px;
+}
+
+/* Related posts item */
+
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post {
+ float: left;
+ width: 33%;
+ margin: 0 0 1em; /* Needs to be same as the main outer wrapper for Related Posts */
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+}
+
+#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post {
+ padding-right: 20px;
+ filter: alpha(opacity=80);
+ -moz-opacity: .8;
+ opacity: .8;
+}
+
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:nth-child(3n+4),
+#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post:nth-child(3n+4) {
+ clear: both;
+}
+
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:hover .jp-relatedposts-post-title a {
+ text-decoration: underline;
+}
+
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:hover {
+ filter: alpha(opacity=100);
+ -moz-opacity: 1;
+ opacity: 1;
+}
+
+/* Related posts item content */
+
+#jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+#jp-relatedposts .jp-relatedposts-items p {
+ font-size: 14px;
+ line-height: 20px;
+ margin: 0;
+}
+#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post-nothumbs {
+ position:relative;
+}
+#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post-nothumbs a.jp-relatedposts-post-aoverlay {
+ position:absolute;
+ top:0;
+ bottom:0;
+ left:0;
+ right:0;
+ display:block;
+ border-bottom: 0;
+}
+
+#jp-relatedposts .jp-relatedposts-items p {
+ margin-bottom: 0;
+}
+
+#jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title {
+ text-transform: none;
+ margin: 0;
+ font-family: inherit;
+ display: block;
+ max-width: 100%;
+}
+
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a {
+ font-size: inherit;
+ font-weight: normal;
+ text-decoration: none;
+ filter: alpha(opacity=100);
+ -moz-opacity: 1;
+ opacity: 1;
+}
+
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a:hover {
+ text-decoration: underline;
+}
+
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post img.jp-relatedposts-post-img,
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post span {
+ display: block;
+ max-width: 90%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post img.jp-relatedposts-post-img,
+#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post span {
+ max-width: 100%;
+}
+
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-date,
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-context {
+ opacity: .6;
+}
+
+/* Hide the date by default, but leave the element there if a theme wants to use css to make it visible. */
+.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-date {
+ display: none;
+}
+
+/* Behavior when there are thumbnails in visual mode */
+#jp-relatedposts .jp-relatedposts-items-visual div.jp-relatedposts-post-thumbs p.jp-relatedposts-post-excerpt {
+ display: none;
+}
+
+/* Behavior when there are no thumbnails in visual mode */
+#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post-nothumbs p.jp-relatedposts-post-excerpt {
+ overflow: hidden;
+}
+#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post-nothumbs span {
+ margin-bottom: 1em;
+}
+
+/* List Layout */
+#jp-relatedposts .jp-relatedposts-list .jp-relatedposts-post {
+ clear: both;
+ width: 100%;
+}
+
+#jp-relatedposts .jp-relatedposts-list .jp-relatedposts-post img.jp-relatedposts-post-img {
+ float: left;
+ overflow: hidden;
+ max-width: 33%;
+ margin-right: 3%;
+}
+
+#jp-relatedposts .jp-relatedposts-list h4.jp-relatedposts-post-title {
+ display: inline-block;
+ max-width: 63%;
+}
+
+/*
+ * Responsive
+ */
+
+@media only screen and (max-width: 640px) {
+
+ #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post {
+ width: 50%;
+ }
+
+ #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:nth-child(3n) {
+ clear: left;
+ }
+
+ #jp-relatedposts .jp-relatedposts-items-visual {
+ margin-right: 20px;
+ }
+}
+
+@media only screen and (max-width: 320px) {
+
+ #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post {
+ width: 100%;
+ clear: both;
+ margin: 0 0 1em;
+ }
+
+ #jp-relatedposts .jp-relatedposts-list .jp-relatedposts-post img.jp-relatedposts-post-img,
+ #jp-relatedposts .jp-relatedposts-list h4.jp-relatedposts-post-title {
+ float: none;
+ max-width: 100%;
+ margin-right: 0;
+ }
+}
diff --git a/plugins/jetpack/modules/related-posts/related-posts.js b/plugins/jetpack/modules/related-posts/related-posts.js
new file mode 100644
index 00000000..f8fa781b
--- /dev/null
+++ b/plugins/jetpack/modules/related-posts/related-posts.js
@@ -0,0 +1,331 @@
+/* jshint onevar: false */
+/* globals related_posts_js_options */
+
+/**
+ * Load related posts
+ */
+( function( $ ) {
+ var jprp = {
+ response: null,
+
+ /**
+ * Utility get related posts JSON endpoint from URLs
+ *
+ * @param {string} URL (optional)
+ * @return {string} Endpoint URL
+ */
+ getEndpointURL: function( URL ) {
+ var locationObject,
+ is_customizer =
+ 'undefined' !== typeof wp &&
+ wp.customize &&
+ wp.customize.settings &&
+ wp.customize.settings.url &&
+ wp.customize.settings.url.self;
+
+ // If we're in Customizer, write the correct URL.
+ if ( is_customizer ) {
+ locationObject = document.createElement( 'a' );
+ locationObject.href = wp.customize.settings.url.self;
+ } else {
+ locationObject = document.location;
+ }
+
+ if ( 'string' === typeof URL && URL.match( /^https?:\/\// ) ) {
+ locationObject = document.createElement( 'a' );
+ locationObject.href = URL;
+ }
+
+ var args = 'relatedposts=1';
+ if ( $( '#jp-relatedposts' ).data( 'exclude' ) ) {
+ args += '&relatedposts_exclude=' + $( '#jp-relatedposts' ).data( 'exclude' );
+ }
+
+ if ( is_customizer ) {
+ args += '&jetpackrpcustomize=1';
+ }
+
+ var pathname = locationObject.pathname;
+ if ( '/' !== pathname[ 0 ] ) {
+ pathname = '/' + pathname;
+ }
+
+ if ( '' === locationObject.search ) {
+ return pathname + '?' + args;
+ } else {
+ return pathname + locationObject.search + '&' + args;
+ }
+ },
+
+ getAnchor: function( post, classNames ) {
+ var anchor_title = post.title;
+ if ( '' !== '' + post.excerpt ) {
+ anchor_title += '\n\n' + post.excerpt;
+ }
+
+ var anchor = $( '<a>' );
+
+ anchor.attr( {
+ class: classNames,
+ href: post.url,
+ title: anchor_title,
+ rel: post.rel,
+ 'data-origin': post.url_meta.origin,
+ 'data-position': post.url_meta.position,
+ } );
+
+ var anchor_html = $( '<div>' )
+ .append( anchor )
+ .html();
+ return [ anchor_html.substring( 0, anchor_html.length - 4 ), '</a>' ];
+ },
+
+ generateMinimalHtml: function( posts, options ) {
+ var self = this;
+ var html = '';
+
+ $.each( posts, function( index, post ) {
+ var anchor = self.getAnchor( post, 'jp-relatedposts-post-a' );
+ var classes = 'jp-relatedposts-post jp-relatedposts-post' + index;
+
+ if ( post.classes.length > 0 ) {
+ classes += ' ' + post.classes.join( ' ' );
+ }
+
+ html +=
+ '<p class="' +
+ classes +
+ '" data-post-id="' +
+ post.id +
+ '" data-post-format="' +
+ post.format +
+ '">';
+ html +=
+ '<span class="jp-relatedposts-post-title">' +
+ anchor[ 0 ] +
+ post.title +
+ anchor[ 1 ] +
+ '</span>';
+ if ( options.showDate ) {
+ html += '<span class="jp-relatedposts-post-date">' + post.date + '</span>';
+ }
+ if ( options.showContext ) {
+ html += '<span class="jp-relatedposts-post-context">' + post.context + '</span>';
+ }
+ html += '</p>';
+ } );
+ return (
+ '<div class="jp-relatedposts-items jp-relatedposts-items-minimal jp-relatedposts-' +
+ options.layout +
+ ' ">' +
+ html +
+ '</div>'
+ );
+ },
+
+ generateVisualHtml: function( posts, options ) {
+ var self = this;
+ var html = '';
+
+ $.each( posts, function( index, post ) {
+ var anchor = self.getAnchor( post, 'jp-relatedposts-post-a' );
+ var classes = 'jp-relatedposts-post jp-relatedposts-post' + index;
+
+ if ( post.classes.length > 0 ) {
+ classes += ' ' + post.classes.join( ' ' );
+ }
+
+ if ( ! post.img.src ) {
+ classes += ' jp-relatedposts-post-nothumbs';
+ } else {
+ classes += ' jp-relatedposts-post-thumbs';
+ }
+
+ html +=
+ '<div class="' +
+ classes +
+ '" data-post-id="' +
+ post.id +
+ '" data-post-format="' +
+ post.format +
+ '">';
+ if ( post.img.src ) {
+ html +=
+ anchor[ 0 ] +
+ '<img class="jp-relatedposts-post-img" src="' +
+ post.img.src +
+ '" width="' +
+ post.img.width +
+ '" alt="' +
+ post.title +
+ '" />' +
+ anchor[ 1 ];
+ } else {
+ var anchor_overlay = self.getAnchor(
+ post,
+ 'jp-relatedposts-post-a jp-relatedposts-post-aoverlay'
+ );
+ html += anchor_overlay[ 0 ] + anchor_overlay[ 1 ];
+ }
+ html +=
+ '<' +
+ related_posts_js_options.post_heading +
+ ' class="jp-relatedposts-post-title">' +
+ anchor[ 0 ] +
+ post.title +
+ anchor[ 1 ] +
+ '</' +
+ related_posts_js_options.post_heading +
+ '>';
+ html +=
+ '<p class="jp-relatedposts-post-excerpt">' +
+ $( '<p>' )
+ .text( post.excerpt )
+ .html() +
+ '</p>';
+ if ( options.showDate ) {
+ html += '<p class="jp-relatedposts-post-date">' + post.date + '</p>';
+ }
+ if ( options.showContext ) {
+ html += '<p class="jp-relatedposts-post-context">' + post.context + '</p>';
+ }
+ html += '</div>';
+ } );
+ return (
+ '<div class="jp-relatedposts-items jp-relatedposts-items-visual jp-relatedposts-' +
+ options.layout +
+ ' ">' +
+ html +
+ '</div>'
+ );
+ },
+
+ /**
+ * We want to set a max height on the excerpt however we want to set
+ * this according to the natual pacing of the page as we never want to
+ * cut off a line of text in the middle so we need to do some detective
+ * work.
+ */
+ setVisualExcerptHeights: function() {
+ var elements = $(
+ '#jp-relatedposts .jp-relatedposts-post-nothumbs .jp-relatedposts-post-excerpt'
+ );
+
+ if ( 0 >= elements.length ) {
+ return;
+ }
+
+ var fontSize = parseInt( elements.first().css( 'font-size' ), 10 ),
+ lineHeight = parseInt( elements.first().css( 'line-height' ), 10 );
+
+ // Show 5 lines of text
+ elements.css( 'max-height', ( 5 * lineHeight ) / fontSize + 'em' );
+ },
+
+ getTrackedUrl: function( anchor ) {
+ var args = 'relatedposts_hit=1';
+ args += '&relatedposts_origin=' + $( anchor ).data( 'origin' );
+ args += '&relatedposts_position=' + $( anchor ).data( 'position' );
+
+ var pathname = anchor.pathname;
+ if ( '/' !== pathname[ 0 ] ) {
+ pathname = '/' + pathname;
+ }
+
+ if ( '' === anchor.search ) {
+ return pathname + '?' + args;
+ } else {
+ return pathname + anchor.search + '&' + args;
+ }
+ },
+
+ cleanupTrackedUrl: function() {
+ if ( 'function' !== typeof history.replaceState ) {
+ return;
+ }
+
+ var cleaned_search = document.location.search.replace(
+ /\brelatedposts_[a-z]+=[0-9]*&?\b/gi,
+ ''
+ );
+ if ( '?' === cleaned_search ) {
+ cleaned_search = '';
+ }
+ if ( document.location.search !== cleaned_search ) {
+ history.replaceState( {}, document.title, document.location.pathname + cleaned_search );
+ }
+ },
+ };
+
+ function afterPostsHaveLoaded() {
+ jprp.setVisualExcerptHeights();
+ $( '#jp-relatedposts a.jp-relatedposts-post-a' ).click( function() {
+ this.href = jprp.getTrackedUrl( this );
+ } );
+ }
+
+ /**
+ * Initialize Related Posts.
+ */
+ function startRelatedPosts() {
+ jprp.cleanupTrackedUrl();
+
+ var endpointURL = jprp.getEndpointURL(),
+ $relatedPosts = $( '#jp-relatedposts' );
+
+ if ( $( '#jp-relatedposts .jp-relatedposts-post' ).length ) {
+ afterPostsHaveLoaded();
+ return;
+ }
+
+ $.getJSON( endpointURL, function( response ) {
+ if ( 0 === response.items.length || 0 === $relatedPosts.length ) {
+ return;
+ }
+
+ jprp.response = response;
+
+ var html,
+ showThumbnails,
+ options = {};
+
+ if ( 'undefined' !== typeof wp && wp.customize ) {
+ showThumbnails = wp.customize.instance( 'jetpack_relatedposts[show_thumbnails]' ).get();
+ options.showDate = wp.customize.instance( 'jetpack_relatedposts[show_date]' ).get();
+ options.showContext = wp.customize.instance( 'jetpack_relatedposts[show_context]' ).get();
+ options.layout = wp.customize.instance( 'jetpack_relatedposts[layout]' ).get();
+ } else {
+ showThumbnails = response.show_thumbnails;
+ options.showDate = response.show_date;
+ options.showContext = response.show_context;
+ options.layout = response.layout;
+ }
+
+ html = ! showThumbnails
+ ? jprp.generateMinimalHtml( response.items, options )
+ : jprp.generateVisualHtml( response.items, options );
+
+ $relatedPosts.append( html );
+ if ( options.showDate ) {
+ $relatedPosts.find( '.jp-relatedposts-post-date' ).show();
+ }
+ $relatedPosts.show();
+ afterPostsHaveLoaded();
+ } );
+ }
+
+ $( function() {
+ if ( 'undefined' !== typeof wp && wp.customize ) {
+ if ( wp.customize.selectiveRefresh ) {
+ wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
+ if ( 'jetpack_relatedposts' === placement.partial.id ) {
+ startRelatedPosts();
+ }
+ } );
+ }
+ wp.customize.bind( 'preview-ready', startRelatedPosts );
+ } else {
+ startRelatedPosts();
+ }
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/related-posts/rtl/related-posts-rtl.css b/plugins/jetpack/modules/related-posts/rtl/related-posts-rtl.css
new file mode 100644
index 00000000..f50c41f3
--- /dev/null
+++ b/plugins/jetpack/modules/related-posts/rtl/related-posts-rtl.css
@@ -0,0 +1,190 @@
+/* This file was automatically generated on Dec 01 2014 22:02:36 */
+
+/**
+ * Styles for Jetpack related posts
+ */
+
+/* Container */
+
+div#jp-relatedposts {
+ display: none;
+ padding-top: 1em;
+ margin: 1em 0;
+ position: relative;
+}
+
+div.jp-relatedposts:after {
+ content: '';
+ display: block;
+ clear: both;
+}
+
+/* Headline above related posts section, labeled "Related" */
+
+div#jp-relatedposts h3.jp-relatedposts-headline {
+ margin: 0 0 1em 0;
+ display: inline-block;
+ float: right;
+ font-size: 9pt;
+ font-weight: bold;
+ font-family: inherit;
+}
+
+div#jp-relatedposts h3.jp-relatedposts-headline em:before {
+ content: "";
+ display: block;
+ width: 100%;
+ min-width: 30px;
+ border-top: 1px solid #ddd;
+ border-top: 1px solid rgba(0,0,0,.2);
+ margin-bottom: 1em;
+}
+
+div#jp-relatedposts h3.jp-relatedposts-headline em {
+ font-style: normal;
+ font-weight: bold;
+}
+
+/* Related posts items (wrapping items) */
+
+div#jp-relatedposts div.jp-relatedposts-items {
+ clear: right;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items-visual {
+ margin-left: -20px;
+}
+
+/* Related posts item */
+
+div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post {
+ float: right;
+ width: 33%;
+ margin: 0 0 1em; /* Needs to be same as the main outer wrapper for Related Posts */
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items-visual .jp-relatedposts-post {
+ padding-left: 20px;
+ /*cursor: pointer;*/
+ filter: alpha(opacity=80);
+ -moz-opacity: .8;
+ opacity: .8;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items div.jp-relatedposts-post:hover .jp-relatedposts-post-title a {
+ text-decoration: underline;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items div.jp-relatedposts-post:hover {
+ filter: alpha(opacity=100);
+ -moz-opacity: 1;
+ opacity: 1;
+}
+
+/* Related posts item content */
+
+div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 14px;
+ line-height: 20px;
+ margin: 0;
+}
+div#jp-relatedposts div.jp-relatedposts-items-visual div.jp-relatedposts-post-nothumbs {
+ position:relative;
+}
+div#jp-relatedposts div.jp-relatedposts-items-visual div.jp-relatedposts-post-nothumbs a.jp-relatedposts-post-aoverlay {
+ position:absolute;
+ top:0;
+ bottom:0;
+ right:0;
+ left:0;
+ display:block;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items p {
+ margin-bottom: 0;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title {
+ text-transform: none;
+ margin: 0;
+ font-family: inherit;
+ display: block;
+ max-width: 100%;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a {
+ font-size: inherit;
+ font-weight: normal;
+ text-decoration: none;
+ filter: alpha(opacity=100);
+ -moz-opacity: 1;
+ opacity: 1;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a:hover {
+ text-decoration: underline;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post img.jp-relatedposts-post-img,
+div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post span {
+ display: block;
+ max-width: 90%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items-visual .jp-relatedposts-post img.jp-relatedposts-post-img,
+div#jp-relatedposts div.jp-relatedposts-items-visual .jp-relatedposts-post span {
+ max-width: 100%;
+}
+
+div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-context {
+ opacity: .6;
+}
+
+/* Behavior when there are thumbnails in visual mode */
+div#jp-relatedposts div.jp-relatedposts-items-visual div.jp-relatedposts-post-thumbs p.jp-relatedposts-post-excerpt {
+ display: none;
+}
+
+/* Behavior when there are no thumbnails in visual mode */
+div#jp-relatedposts div.jp-relatedposts-items-visual div.jp-relatedposts-post-nothumbs p.jp-relatedposts-post-excerpt {
+ overflow: hidden;
+}
+div#jp-relatedposts div.jp-relatedposts-items-visual div.jp-relatedposts-post-nothumbs span {
+ margin-bottom: 1em;
+}
+
+/**
+ * Responsive
+ */
+
+@media only screen and (max-width: 640px) {
+
+ div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post {
+ width: 50%;
+ }
+
+ div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post:nth-child(3n) {
+ clear: right;
+ }
+
+ div#jp-relatedposts div.jp-relatedposts-items-visual {
+ margin-left: 20px;
+ }
+
+}
+
+@media only screen and (max-width: 320px) {
+
+ div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post {
+ width: 100%;
+ clear: both;
+ margin: 0 0 1em;
+ }
+
+}
diff --git a/plugins/jetpack/modules/search.php b/plugins/jetpack/modules/search.php
new file mode 100644
index 00000000..0f94315b
--- /dev/null
+++ b/plugins/jetpack/modules/search.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Module Name: Search
+ * Module Description: Enhanced search, powered by Elasticsearch, a powerful replacement for WordPress search.
+ * First Introduced: 5.0
+ * Sort Order: 34
+ * Free: false
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Feature: Search
+ * Additional Search Queries: search, elastic, elastic search, elasticsearch, fast search, search results, search performance, google search
+ * Plans: business
+ */
+
+require_once( dirname( __FILE__ ) . '/search/class.jetpack-search.php' );
+
+Jetpack_Search::instance();
diff --git a/plugins/jetpack/modules/search/class.jetpack-search-helpers.php b/plugins/jetpack/modules/search/class.jetpack-search-helpers.php
new file mode 100644
index 00000000..27526b4c
--- /dev/null
+++ b/plugins/jetpack/modules/search/class.jetpack-search-helpers.php
@@ -0,0 +1,699 @@
+<?php
+/**
+ * Jetpack Search: Jetpack_Search_Helpers class
+ *
+ * @package Jetpack
+ * @subpackage Jetpack Search
+ * @since 5.8.0
+ */
+
+/**
+ * Various helper functions for reuse throughout the Jetpack Search code.
+ *
+ * @since 5.8.0
+ */
+class Jetpack_Search_Helpers {
+
+ /**
+ * The search widget's base ID.
+ *
+ * @since 5.8.0
+ * @var string
+ */
+ const FILTER_WIDGET_BASE = 'jetpack-search-filters';
+
+ /**
+ * Create a URL for the current search that doesn't include the "paged" parameter.
+ *
+ * @since 5.8.0
+ *
+ * @return string The search URL.
+ */
+ static function get_search_url() {
+ $query_args = stripslashes_deep( $_GET );
+
+ // Handle the case where a permastruct is being used, such as /search/{$query}
+ if ( ! isset( $query_args['s'] ) ) {
+ $query_args['s'] = get_search_query();
+ }
+
+ if ( isset( $query_args['paged'] ) ) {
+ unset( $query_args['paged'] );
+ }
+
+ $query = http_build_query( $query_args );
+
+ return home_url( "?{$query}" );
+ }
+
+ /**
+ * Wraps add_query_arg() with the URL defaulting to the current search URL.
+ *
+ * @see add_query_arg()
+ *
+ * @since 5.8.0
+ *
+ * @param string|array $key Either a query variable key, or an associative array of query variables.
+ * @param string $value Optional. A query variable value.
+ * @param bool|string $url Optional. A URL to act upon. Defaults to the current search URL.
+ *
+ * @return string New URL query string (unescaped).
+ */
+ static function add_query_arg( $key, $value = false, $url = false ) {
+ $url = empty( $url ) ? self::get_search_url() : $url;
+ if ( is_array( $key ) ) {
+ return add_query_arg( $key, $url );
+ }
+
+ return add_query_arg( $key, $value, $url );
+ }
+
+ /**
+ * Wraps remove_query_arg() with the URL defaulting to the current search URL.
+ *
+ * @see remove_query_arg()
+ *
+ * @since 5.8.0
+ *
+ * @param string|array $key Query key or keys to remove.
+ * @param bool|string $query Optional. A URL to act upon. Defaults to the current search URL.
+ *
+ * @return string New URL query string (unescaped).
+ */
+ static function remove_query_arg( $key, $url = false ) {
+ $url = empty( $url ) ? self::get_search_url() : $url;
+
+ return remove_query_arg( $key, $url );
+ }
+
+ /**
+ * Returns the name of the search widget's option.
+ *
+ * @since 5.8.0
+ *
+ * @return string The search widget option name.
+ */
+ static function get_widget_option_name() {
+ return sprintf( 'widget_%s', self::FILTER_WIDGET_BASE );
+ }
+
+ /**
+ * Returns the search widget instances from the widget's option.
+ *
+ * @since 5.8.0
+ *
+ * @return array The widget options.
+ */
+ static function get_widgets_from_option() {
+ $widget_options = get_option( self::get_widget_option_name(), array() );
+
+ // We don't need this
+ if ( ! empty( $widget_options ) && isset( $widget_options['_multiwidget'] ) ) {
+ unset( $widget_options['_multiwidget'] );
+ }
+
+ return $widget_options;
+ }
+
+ /**
+ * Returns the widget ID (widget base plus the numeric ID).
+ *
+ * @param int $number The widget's numeric ID.
+ *
+ * @return string The widget's numeric ID prefixed with the search widget base.
+ */
+ static function build_widget_id( $number ) {
+ return sprintf( '%s-%d', self::FILTER_WIDGET_BASE, $number );
+ }
+
+ /**
+ * Wrapper for is_active_widget() with the other parameters automatically supplied.
+ *
+ * @see is_active_widget()
+ *
+ * @since 5.8.0
+ *
+ * @param int $widget_id Widget ID.
+ *
+ * @return bool Whether the widget is active or not.
+ */
+ static function is_active_widget( $widget_id ) {
+ return (bool) is_active_widget( false, $widget_id, self::FILTER_WIDGET_BASE, true );
+ }
+
+ /**
+ * Returns an array of the filters from all active search widgets.
+ *
+ * @since 5.8.0
+ *
+ * @return array Active filters.
+ */
+ static function get_filters_from_widgets() {
+ $filters = array();
+
+ $widget_options = self::get_widgets_from_option();
+ if ( empty( $widget_options ) ) {
+ return $filters;
+ }
+
+ foreach ( (array) $widget_options as $number => $settings ) {
+ $widget_id = self::build_widget_id( $number );
+ if ( ! self::is_active_widget( $widget_id ) || empty( $settings['filters'] ) ) {
+ continue;
+ }
+
+ foreach ( (array) $settings['filters'] as $widget_filter ) {
+ $widget_filter['widget_id'] = $widget_id;
+
+ if ( empty( $widget_filter['name'] ) ) {
+ $widget_filter['name'] = self::generate_widget_filter_name( $widget_filter );
+ }
+
+ $key = sprintf( '%s_%d', $widget_filter['type'], count( $filters ) );
+
+ $filters[ $key ] = $widget_filter;
+ }
+ }
+
+ return $filters;
+ }
+
+ /**
+ * Get the localized default label for a date filter.
+ *
+ * @since 5.8.0
+ *
+ * @param string $type Date type, either year or month.
+ * @param bool $is_updated Whether the filter was updated or not (adds "Updated" to the end).
+ *
+ * @return string The filter label.
+ */
+ static function get_date_filter_type_name( $type, $is_updated = false ) {
+ switch ( $type ) {
+ case 'year':
+ $string = ( $is_updated )
+ ? esc_html_x( 'Year Updated', 'label for filtering posts', 'jetpack' )
+ : esc_html_x( 'Year', 'label for filtering posts', 'jetpack' );
+ break;
+ case 'month':
+ default:
+ $string = ( $is_updated )
+ ? esc_html_x( 'Month Updated', 'label for filtering posts', 'jetpack' )
+ : esc_html_x( 'Month', 'label for filtering posts', 'jetpack' );
+ break;
+ }
+
+ return $string;
+ }
+
+ /**
+ * Creates a default name for a filter. Used when the filter label is blank.
+ *
+ * @since 5.8.0
+ *
+ * @param array $widget_filter The filter to generate the title for.
+ *
+ * @return string The suggested filter name.
+ */
+ static function generate_widget_filter_name( $widget_filter ) {
+ $name = '';
+
+ switch ( $widget_filter['type'] ) {
+ case 'post_type':
+ $name = _x( 'Post Types', 'label for filtering posts', 'jetpack' );
+ break;
+
+ case 'date_histogram':
+ $modified_fields = array(
+ 'post_modified',
+ 'post_modified_gmt',
+ );
+ switch ( $widget_filter['interval'] ) {
+ case 'year':
+ $name = self::get_date_filter_type_name(
+ 'year',
+ in_array( $widget_filter['field'], $modified_fields )
+ );
+ break;
+ case 'month':
+ default:
+ $name = self::get_date_filter_type_name(
+ 'month',
+ in_array( $widget_filter['field'], $modified_fields )
+ );
+ break;
+ }
+ break;
+
+ case 'taxonomy':
+ $tax = get_taxonomy( $widget_filter['taxonomy'] );
+ if ( ! $tax ) {
+ break;
+ }
+
+ if ( isset( $tax->label ) ) {
+ $name = $tax->label;
+ } elseif ( isset( $tax->labels ) && isset( $tax->labels->name ) ) {
+ $name = $tax->labels->name;
+ }
+ break;
+ }
+
+ return $name;
+ }
+
+ /**
+ * Whether we should rerun a search in the customizer preview or not.
+ *
+ * @since 5.8.0
+ *
+ * @return bool
+ */
+ static function should_rerun_search_in_customizer_preview() {
+ // Only update when in a customizer preview and data is being posted.
+ // Check for $_POST removes an extra update when the customizer loads.
+ //
+ // Note: We use $GLOBALS['wp_customize'] here instead of is_customize_preview() to support unit tests.
+ if ( ! isset( $GLOBALS['wp_customize'] ) || ! $GLOBALS['wp_customize']->is_preview() || empty( $_POST ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Since PHP's built-in array_diff() works by comparing the values that are in array 1 to the other arrays,
+ * if there are less values in array 1, it's possible to get an empty diff where one might be expected.
+ *
+ * @since 5.8.0
+ *
+ * @param array $array_1
+ * @param array $array_2
+ *
+ * @return array
+ */
+ static function array_diff( $array_1, $array_2 ) {
+ // If the array counts are the same, then the order doesn't matter. If the count of
+ // $array_1 is higher than $array_2, that's also fine. If the count of $array_2 is higher,
+ // we need to swap the array order though.
+ if ( count( $array_1 ) !== count( $array_2 ) && count( $array_2 ) > count( $array_1 ) ) {
+ $temp = $array_1;
+ $array_1 = $array_2;
+ $array_2 = $temp;
+ }
+
+ // Disregard keys
+ return array_values( array_diff( $array_1, $array_2 ) );
+ }
+
+ /**
+ * Given the widget instance, will return true when selected post types differ from searchable post types.
+ *
+ * @since 5.8.0
+ *
+ * @param array $post_types An array of post types.
+ *
+ * @return bool
+ */
+ static function post_types_differ_searchable( $post_types ) {
+ if ( empty( $post_types ) ) {
+ return false;
+ }
+
+ $searchable_post_types = get_post_types( array( 'exclude_from_search' => false ) );
+ $diff_of_searchable = self::array_diff( $searchable_post_types, (array) $post_types );
+
+ return ! empty( $diff_of_searchable );
+ }
+
+ /**
+ * Given the array of post types, will return true when these differ from the current search query.
+ *
+ * @since 5.8.0
+ *
+ * @param array $post_types An array of post types.
+ *
+ * @return bool
+ */
+ static function post_types_differ_query( $post_types ) {
+ if ( empty( $post_types ) ) {
+ return false;
+ }
+
+ if ( empty( $_GET['post_type'] ) ) {
+ $post_types_from_query = array();
+ } elseif ( is_array( $_GET['post_type'] ) ) {
+ $post_types_from_query = $_GET['post_type'];
+ } else {
+ $post_types_from_query = (array) explode( ',', $_GET['post_type'] );
+ }
+
+ $post_types_from_query = array_map( 'trim', $post_types_from_query );
+
+ $diff_query = self::array_diff( (array) $post_types, $post_types_from_query );
+
+ return ! empty( $diff_query );
+ }
+
+ /**
+ * Determine what Tracks value should be used when updating a widget.
+ *
+ * @since 5.8.0
+ *
+ * @param mixed $old_value The old option value.
+ * @param mixed $new_value The new option value.
+ *
+ * @return array|false False if the widget wasn't updated, otherwise an array of the Tracks action and widget properties.
+ */
+ static function get_widget_tracks_value( $old_value, $new_value ) {
+ $old_value = (array) $old_value;
+ if ( isset( $old_value['_multiwidget'] ) ) {
+ unset( $old_value['_multiwidget'] );
+ }
+
+ $new_value = (array) $new_value;
+ if ( isset( $new_value['_multiwidget'] ) ) {
+ unset( $new_value['_multiwidget'] );
+ }
+
+ $old_keys = array_keys( $old_value );
+ $new_keys = array_keys( $new_value );
+
+ if ( count( $new_keys ) > count( $old_keys ) ) { // This is the case for a widget being added
+ $diff = self::array_diff( $new_keys, $old_keys );
+ $action = 'widget_added';
+ $widget = empty( $diff ) || ! isset( $new_value[ $diff[0] ] )
+ ? false
+ : $new_value[ $diff[0] ];
+ } elseif ( count( $old_keys ) > count( $new_keys ) ) { // This is the case for a widget being deleted
+ $diff = self::array_diff( $old_keys, $new_keys );
+ $action = 'widget_deleted';
+ $widget = empty( $diff ) || ! isset( $old_value[ $diff[0] ] )
+ ? false
+ : $old_value[ $diff[0] ];
+ } else {
+ $action = 'widget_updated';
+ $widget = false;
+
+ // This is a bit crazy. Since there can be multiple widgets stored in a single option,
+ // we need to diff the old and new values to figure out which widget was updated.
+ foreach ( $new_value as $key => $new_instance ) {
+ if ( ! isset( $old_value[ $key ] ) ) {
+ continue;
+ }
+ $old_instance = $old_value[ $key ];
+
+ // First, let's test the keys of each instance
+ $diff = self::array_diff( array_keys( $new_instance ), array_keys( $old_instance ) );
+ if ( ! empty( $diff ) ) {
+ $widget = $new_instance;
+ break;
+ }
+
+ // Next, lets's loop over each value and compare it
+ foreach ( $new_instance as $k => $v ) {
+ if ( is_scalar( $v ) && (string) $v !== (string) $old_instance[ $k ] ) {
+ $widget = $new_instance;
+ break;
+ }
+
+ if ( 'filters' == $k ) {
+ if ( count( $new_instance['filters'] ) != count( $old_instance['filters'] ) ) {
+ $widget = $new_instance;
+ break;
+ }
+
+ foreach ( $v as $filter_key => $new_filter_value ) {
+ $diff = self::array_diff( $new_filter_value, $old_instance['filters'][ $filter_key ] );
+ if ( ! empty( $diff ) ) {
+ $widget = $new_instance;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if ( empty( $action ) || empty( $widget ) ) {
+ return false;
+ }
+
+ return array(
+ 'action' => $action,
+ 'widget' => self::get_widget_properties_for_tracks( $widget ),
+ );
+ }
+
+ /**
+ * Creates the widget properties for sending to Tracks.
+ *
+ * @since 5.8.0
+ *
+ * @param array $widget The widget instance.
+ *
+ * @return array The widget properties.
+ */
+ static function get_widget_properties_for_tracks( $widget ) {
+ $sanitized = array();
+
+ foreach ( (array) $widget as $key => $value ) {
+ if ( '_multiwidget' == $key ) {
+ continue;
+ }
+
+ if ( is_scalar( $value ) ) {
+ $key = str_replace( '-', '_', sanitize_key( $key ) );
+ $key = "widget_{$key}";
+ $sanitized[ $key ] = $value;
+ }
+ }
+
+ $filters_properties = ! empty( $widget['filters'] )
+ ? self::get_filter_properties_for_tracks( $widget['filters'] )
+ : array();
+
+ return array_merge( $sanitized, $filters_properties );
+ }
+
+ /**
+ * Creates the filter properties for sending to Tracks.
+ *
+ * @since 5.8.0
+ *
+ * @param array $filters An array of filters.
+ *
+ * @return array The filter properties.
+ */
+ static function get_filter_properties_for_tracks( $filters ) {
+ if ( empty( $filters ) ) {
+ return $filters;
+ }
+
+ $filters_properties = array(
+ 'widget_filter_count' => count( $filters ),
+ );
+
+ foreach ( $filters as $filter ) {
+ if ( empty( $filter['type'] ) ) {
+ continue;
+ }
+
+ $key = sprintf( 'widget_filter_type_%s', $filter['type'] );
+ if ( isset( $filters_properties[ $key ] ) ) {
+ $filters_properties[ $key ] ++;
+ } else {
+ $filters_properties[ $key ] = 1;
+ }
+ }
+
+ return $filters_properties;
+ }
+
+ /**
+ * Gets the active post types given a set of filters.
+ *
+ * @since 5.8.0
+ *
+ * @param array $filters The active filters for the current query.
+ *
+ * @return array The active post types.
+ */
+ public static function get_active_post_types( $filters ) {
+ $active_post_types = array();
+
+ foreach ( $filters as $item ) {
+ if ( ( 'post_type' == $item['type'] ) && isset( $item['query_vars']['post_type'] ) ) {
+ $active_post_types[] = $item['query_vars']['post_type'];
+ }
+ }
+
+ return $active_post_types;
+ }
+
+ /**
+ * Sets active to false on all post type buckets.
+ *
+ * @since 5.8.0
+ *
+ * @param array $filters The available filters for the current query.
+ *
+ * @return array The filters for the current query with modified active field.
+ */
+ public static function remove_active_from_post_type_buckets( $filters ) {
+ $modified = $filters;
+ foreach ( $filters as $key => $filter ) {
+ if ( 'post_type' === $filter['type'] && ! empty( $filter['buckets'] ) ) {
+ foreach ( $filter['buckets'] as $k => $bucket ) {
+ $bucket['active'] = false;
+ $modified[ $key ]['buckets'][ $k ] = $bucket;
+ }
+ }
+ }
+
+ return $modified;
+ }
+
+ /**
+ * Given a url and an array of post types, will ensure that the post types are properly applied to the URL as args.
+ *
+ * @since 5.8.0
+ *
+ * @param string $url The URL to add post types to.
+ * @param array $post_types An array of post types that should be added to the URL.
+ *
+ * @return string The URL with added post types.
+ */
+ public static function add_post_types_to_url( $url, $post_types ) {
+ $url = Jetpack_Search_Helpers::remove_query_arg( 'post_type', $url );
+ if ( empty( $post_types ) ) {
+ return $url;
+ }
+
+ $url = Jetpack_Search_Helpers::add_query_arg(
+ 'post_type',
+ implode( ',', $post_types ),
+ $url
+ );
+
+ return $url;
+ }
+
+ /**
+ * Since we provide support for the widget restricting post types by adding the selected post types as
+ * active filters, if removing a post type filter would result in there no longer be post_type args in the URL,
+ * we need to be sure to add them back.
+ *
+ * @since 5.8.0
+ *
+ * @param array $filters An array of possible filters for the current query.
+ * @param array $post_types The post types to ensure are on the link.
+ *
+ * @return array The updated array of filters with post typed added to the remove URLs.
+ */
+ public static function ensure_post_types_on_remove_url( $filters, $post_types ) {
+ $modified = $filters;
+
+ foreach ( (array) $filters as $filter_key => $filter ) {
+ if ( 'post_type' !== $filter['type'] || empty( $filter['buckets'] ) ) {
+ $modified[ $filter_key ] = $filter;
+ continue;
+ }
+
+ foreach ( (array) $filter['buckets'] as $bucket_key => $bucket ) {
+ if ( empty( $bucket['remove_url'] ) ) {
+ continue;
+ }
+
+ $parsed = wp_parse_url( $bucket['remove_url'] );
+ if ( ! $parsed ) {
+ continue;
+ }
+
+ $query = array();
+ if ( ! empty( $parsed['query'] ) ) {
+ wp_parse_str( $parsed['query'], $query );
+ }
+
+ if ( empty( $query['post_type'] ) ) {
+ $modified[ $filter_key ]['buckets'][ $bucket_key ]['remove_url'] = self::add_post_types_to_url(
+ $bucket['remove_url'],
+ $post_types
+ );
+ }
+ }
+ }
+
+ return $modified;
+ }
+
+ /**
+ * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
+ * developers to disable filters supplied by the search widget. Useful if filters are
+ * being defined at the code level.
+ *
+ * @since 5.8.0
+ *
+ * @return bool
+ */
+ public static function are_filters_by_widget_disabled() {
+ /**
+ * Allows developers to disable filters being set by widget, in favor of manually
+ * setting filters via `Jetpack_Search::set_filters()`.
+ *
+ * @module search
+ *
+ * @since 5.7.0
+ *
+ * @param bool false
+ */
+ return apply_filters( 'jetpack_search_disable_widget_filters', false );
+ }
+
+ /**
+ * Returns a boolean for whether the current site has a VIP index.
+ *
+ * @since 5.8.0
+ *
+ * @return bool
+ */
+ public static function site_has_vip_index() {
+ $has_vip_index = (
+ Jetpack_Constants::is_defined( 'JETPACK_SEARCH_VIP_INDEX' ) &&
+ Jetpack_Constants::get_constant( 'JETPACK_SEARCH_VIP_INDEX' )
+ );
+
+ /**
+ * Allows developers to filter whether the current site has a VIP index.
+ *
+ * @module search
+ *
+ * @since 5.8.0
+ *
+ * @param bool $has_vip_index Whether the current site has a VIP index.
+ */
+ return apply_filters( 'jetpack_search_has_vip_index', $has_vip_index );
+ }
+
+ /**
+ * Returns the maximum posts per page for a search query.
+ *
+ * @since 5.8.0
+ *
+ * @return int
+ */
+ public static function get_max_posts_per_page() {
+ return self::site_has_vip_index() ? 1000 : 100;
+ }
+
+ /**
+ * Returns the maximum offset for a search query.
+ *
+ * @since 5.8.0
+ *
+ * @return int
+ */
+ public static function get_max_offset() {
+ return self::site_has_vip_index() ? 9000 : 1000;
+ }
+}
diff --git a/plugins/jetpack/modules/search/class.jetpack-search-template-tags.php b/plugins/jetpack/modules/search/class.jetpack-search-template-tags.php
new file mode 100644
index 00000000..303e6caa
--- /dev/null
+++ b/plugins/jetpack/modules/search/class.jetpack-search-template-tags.php
@@ -0,0 +1,225 @@
+<?php
+/**
+ * Jetpack Search: Jetpack_Search_Template_Tags class
+ *
+ * @package Jetpack
+ * @subpackage Jetpack Search
+ * @since 5.8.0
+ */
+
+/**
+ * Class that has various methods for outputting functionality into a theme that doesn't support widgets.
+ * Additionally the widget itself makes use of these class.
+ *
+ * @since 5.8.0
+ */
+class Jetpack_Search_Template_Tags {
+
+ /**
+ * Renders all available filters that can be used to filter down search results on the frontend.
+ *
+ * @since 5.8.0
+ *
+ * @param array $filters The available filters for the current query.
+ * @param array $post_types An array of post types to make filterable
+ */
+ public static function render_available_filters( $filters = null, $post_types = null ) {
+ if ( is_null( $filters ) ) {
+ $filters = Jetpack_Search::instance()->get_filters();
+ }
+
+ if ( is_null( $post_types ) ) {
+ $post_types = get_post_types( array( 'exclude_from_search' => false ) );
+ }
+
+ /**
+ * If the post types specified by the widget differ from the default set of searchable post types,
+ * then we need to track their state.
+ */
+ $active_post_types = array();
+ if ( Jetpack_Search_Helpers::post_types_differ_searchable( $post_types ) ) {
+ // get the active filter buckets from the query
+ $active_buckets = Jetpack_Search::instance()->get_active_filter_buckets();
+ $post_types_differ_query = Jetpack_Search_Helpers::post_types_differ_query( $post_types );
+
+ // remove any post_type filters from display if the current query
+ // already specifies to match all post types
+ if ( ! $post_types_differ_query ) {
+ $active_buckets = array_filter( $active_buckets, array( __CLASS__, 'is_not_post_type_filter' ) );
+ }
+
+ $active_post_types = Jetpack_Search_Helpers::get_active_post_types( $active_buckets );
+ if ( empty( $active_post_types ) ) {
+ $active_post_types = $post_types;
+ }
+
+ if ( $post_types_differ_query ) {
+ $filters = Jetpack_Search_Helpers::ensure_post_types_on_remove_url( $filters, $post_types );
+ } else {
+ $filters = Jetpack_Search_Helpers::remove_active_from_post_type_buckets( $filters );
+ }
+ } else {
+ $post_types = array();
+ }
+
+ foreach ( (array) $filters as $filter ) {
+ if ( 'post_type' == $filter['type'] ) {
+ self::render_filter( $filter, $post_types );
+ } else {
+ self::render_filter( $filter, $active_post_types );
+ }
+ }
+ }
+
+ /**
+ * Renders a single filter that can be applied to the current search.
+ *
+ * @since 5.8.0
+ *
+ * @param array $filter The filter to render.
+ * @param array $default_post_types The default post types for this filter.
+ */
+ public static function render_filter( $filter, $default_post_types ) {
+ if ( empty( $filter ) || empty( $filter['buckets'] ) ) {
+ return;
+ }
+
+ $query_vars = null;
+ foreach ( $filter['buckets'] as $item ) {
+ if ( $item['active'] ) {
+ $query_vars = array_keys( $item['query_vars'] );
+ break;
+ }
+ }
+ $clear_url = null;
+ if ( ! empty( $query_vars ) ) {
+ $clear_url = Jetpack_Search_Helpers::remove_query_arg( $query_vars );
+ if ( ! empty( $default_post_types ) ) {
+ $clear_url = Jetpack_Search_Helpers::add_post_types_to_url( $clear_url, $default_post_types );
+ }
+ }
+
+ ?>
+ <h4 class="jetpack-search-filters-widget__sub-heading">
+ <?php echo esc_html( $filter['name'] ); ?>
+ </h4>
+ <?php if ( $clear_url ) : ?>
+ <div class="jetpack-search-filters-widget__clear">
+ <a href="<?php echo esc_url( $clear_url ); ?>">
+ <?php esc_html_e( '< Clear Filters', 'jetpack' ); ?>
+ </a>
+ </div>
+ <?php endif; ?>
+ <ul class="jetpack-search-filters-widget__filter-list">
+ <?php
+ foreach ( $filter['buckets'] as $item ) :
+ $url = ( empty( $item['active'] ) ) ? $item['url'] : $item['remove_url'];
+ ?>
+ <li>
+ <label>
+ <input type="checkbox"<?php checked( ! empty( $item['active'] ) ); ?> disabled="disabled" />&nbsp;
+ <a href="<?php echo esc_url( $url ); ?>">
+ <?php
+ echo esc_html( $item['name'] );
+ echo '&nbsp;';
+ echo esc_html( sprintf(
+ '(%s)',
+ number_format_i18n( absint( $item['count'] ) )
+ ) );
+ ?>
+ </a>
+ </label>
+ </li>
+ <?php endforeach; ?>
+ </ul>
+ <?php
+ }
+
+ /**
+ * Outputs the search widget's title.
+ *
+ * @since 5.8.0
+ *
+ * @param string $title The widget's title
+ * @param string $before_title The HTML tag to display before the title
+ * @param string $after_title The HTML tag to display after the title
+ */
+ public static function render_widget_title( $title, $before_title, $after_title ) {
+ echo $before_title . esc_html( $title ) . $after_title;
+ }
+
+ /**
+ * Responsible for rendering the search box within our widget on the frontend.
+ *
+ * @since 5.8.0
+ *
+ * @param array $post_types Array of post types to limit search results to.
+ * @param string $orderby How to order the search results.
+ * @param string $order In what direction to order the search results.
+ */
+ public static function render_widget_search_form( $post_types, $orderby, $order ) {
+ $form = get_search_form( false );
+
+ $fields_to_inject = array(
+ 'orderby' => $orderby,
+ 'order' => $order
+ );
+
+ // If the widget has specified post types to search within and IF the post types differ
+ // from the default post types that would have been searched, set the selected post
+ // types via hidden inputs.
+ if ( Jetpack_Search_Helpers::post_types_differ_searchable( $post_types ) ) {
+ $fields_to_inject['post_type'] = implode( ',', $post_types );
+ }
+
+ $form = self::inject_hidden_form_fields( $form, $fields_to_inject );
+
+ echo '<div class="jetpack-search-form">';
+ echo $form;
+ echo '</div>';
+ }
+
+ /**
+ * Modifies an HTML form to add some additional hidden fields.
+ *
+ * @since 5.8.0
+ *
+ * @param string $form The form HTML to modify.
+ * @param array $fields Array of hidden fields to add. Key is field name and value is the field value.
+ *
+ * @return string The modified form HTML.
+ */
+ private static function inject_hidden_form_fields( $form, $fields ) {
+ $form_injection = '';
+
+ foreach ( $fields as $field_name => $field_value ) {
+ $form_injection .= sprintf(
+ '<input type="hidden" name="%s" value="%s" />',
+ esc_attr( $field_name ),
+ esc_attr( $field_value )
+ );
+ }
+
+ // This shouldn't need to be escaped since we've escaped above as we built $form_injection
+ $form = str_replace(
+ '</form>',
+ $form_injection . '</form>',
+ $form
+ );
+
+ return $form;
+ }
+
+ /**
+ * Internal method for filtering out non-post_type filters.
+ *
+ * @since 5.8.0
+ *
+ * @param array $filter
+ *
+ * @return bool
+ */
+ private static function is_not_post_type_filter( $filter ) {
+ return 'post_type' !== $filter['type'];
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/search/class.jetpack-search.php b/plugins/jetpack/modules/search/class.jetpack-search.php
new file mode 100644
index 00000000..40867589
--- /dev/null
+++ b/plugins/jetpack/modules/search/class.jetpack-search.php
@@ -0,0 +1,1874 @@
+<?php
+/**
+ * Jetpack Search: Main Jetpack_Search class
+ *
+ * @package Jetpack
+ * @subpackage Jetpack Search
+ * @since 5.0.0
+ */
+
+/**
+ * The main class for the Jetpack Search module.
+ *
+ * @since 5.0.0
+ */
+class Jetpack_Search {
+
+ /**
+ * The number of found posts.
+ *
+ * @since 5.0.0
+ *
+ * @var int
+ */
+ protected $found_posts = 0;
+
+ /**
+ * The search result, as returned by the WordPress.com REST API.
+ *
+ * @since 5.0.0
+ *
+ * @var array
+ */
+ protected $search_result;
+
+ /**
+ * This site's blog ID on WordPress.com.
+ *
+ * @since 5.0.0
+ *
+ * @var int
+ */
+ protected $jetpack_blog_id;
+
+ /**
+ * The Elasticsearch aggregations (filters).
+ *
+ * @since 5.0.0
+ *
+ * @var array
+ */
+ protected $aggregations = array();
+
+ /**
+ * The maximum number of aggregations allowed.
+ *
+ * @since 5.0.0
+ *
+ * @var int
+ */
+ protected $max_aggregations_count = 100;
+
+ /**
+ * Statistics about the last Elasticsearch query.
+ *
+ * @since 5.6.0
+ *
+ * @var array
+ */
+ protected $last_query_info = array();
+
+ /**
+ * Statistics about the last Elasticsearch query failure.
+ *
+ * @since 5.6.0
+ *
+ * @var array
+ */
+ protected $last_query_failure_info = array();
+
+ /**
+ * The singleton instance of this class.
+ *
+ * @since 5.0.0
+ *
+ * @var Jetpack_Search
+ */
+ protected static $instance;
+
+ /**
+ * Languages with custom analyzers. Other languages are supported, but are analyzed with the default analyzer.
+ *
+ * @since 5.0.0
+ *
+ * @var array
+ */
+ public static $analyzed_langs = array( 'ar', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fi', 'fr', 'he', 'hi', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' );
+
+ /**
+ * Jetpack_Search constructor.
+ *
+ * @since 5.0.0
+ *
+ * Doesn't do anything. This class needs to be initialized via the instance() method instead.
+ */
+ protected function __construct() {
+ }
+
+ /**
+ * Prevent __clone()'ing of this class.
+ *
+ * @since 5.0.0
+ */
+ public function __clone() {
+ wp_die( "Please don't __clone Jetpack_Search" );
+ }
+
+ /**
+ * Prevent __wakeup()'ing of this class.
+ *
+ * @since 5.0.0
+ */
+ public function __wakeup() {
+ wp_die( "Please don't __wakeup Jetpack_Search" );
+ }
+
+ /**
+ * Get singleton instance of Jetpack_Search.
+ *
+ * Instantiates and sets up a new instance if needed, or returns the singleton.
+ *
+ * @since 5.0.0
+ *
+ * @return Jetpack_Search The Jetpack_Search singleton.
+ */
+ public static function instance() {
+ if ( ! isset( self::$instance ) ) {
+ self::$instance = new Jetpack_Search();
+
+ self::$instance->setup();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Perform various setup tasks for the class.
+ *
+ * Checks various pre-requisites and adds hooks.
+ *
+ * @since 5.0.0
+ */
+ public function setup() {
+ if ( ! Jetpack::is_active() || ! Jetpack_Plan::supports( 'search' ) ) {
+ return;
+ }
+
+ $this->jetpack_blog_id = Jetpack::get_option( 'id' );
+
+ if ( ! $this->jetpack_blog_id ) {
+ return;
+ }
+
+ require_once dirname( __FILE__ ) . '/class.jetpack-search-helpers.php';
+ require_once dirname( __FILE__ ) . '/class.jetpack-search-template-tags.php';
+ require_once JETPACK__PLUGIN_DIR . 'modules/widgets/search.php';
+
+ $this->init_hooks();
+ }
+
+ /**
+ * Setup the various hooks needed for the plugin to take over search duties.
+ *
+ * @since 5.0.0
+ */
+ public function init_hooks() {
+ if ( ! is_admin() ) {
+ add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
+
+ add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 );
+
+ add_action( 'did_jetpack_search_query', array( $this, 'store_last_query_info' ) );
+ add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
+
+ add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
+
+ add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
+ } else {
+ add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
+ }
+
+ add_action( 'jetpack_deactivate_module_search', array( $this, 'move_search_widgets_to_inactive' ) );
+ }
+
+ /**
+ * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer.
+ *
+ * @since 5.6.0
+ *
+ * @param array $meta Information about the failure.
+ */
+ public function store_query_failure( $meta ) {
+ $this->last_query_failure_info = $meta;
+ add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
+ }
+
+ /**
+ * Outputs information about the last Elasticsearch failure.
+ *
+ * @since 5.6.0
+ */
+ public function print_query_failure() {
+ if ( $this->last_query_failure_info ) {
+ printf(
+ '<!-- Jetpack Search failed with code %s: %s - %s -->',
+ esc_html( $this->last_query_failure_info['response_code'] ),
+ esc_html( $this->last_query_failure_info['json']['error'] ),
+ esc_html( $this->last_query_failure_info['json']['message'] )
+ );
+ }
+ }
+
+ /**
+ * Stores information about the last Elasticsearch query and enqueues some debug information in the footer.
+ *
+ * @since 5.6.0
+ *
+ * @param array $meta Information about the query.
+ */
+ public function store_last_query_info( $meta ) {
+ $this->last_query_info = $meta;
+ add_action( 'wp_footer', array( $this, 'print_query_success' ) );
+ }
+
+ /**
+ * Outputs information about the last Elasticsearch search.
+ *
+ * @since 5.6.0
+ */
+ public function print_query_success() {
+ if ( $this->last_query_info ) {
+ printf(
+ '<!-- Jetpack Search took %s ms, ES time %s ms -->',
+ intval( $this->last_query_info['elapsed_time'] ),
+ esc_html( $this->last_query_info['es_time'] )
+ );
+
+ if ( isset( $_GET['searchdebug'] ) ) {
+ printf(
+ '<!-- Query response data: %s -->',
+ esc_html( print_r( $this->last_query_info, 1 ) )
+ );
+ }
+ }
+ }
+
+ /**
+ * Returns the last query information, or false if no information was stored.
+ *
+ * @since 5.8.0
+ *
+ * @return bool|array
+ */
+ public function get_last_query_info() {
+ return empty( $this->last_query_info ) ? false : $this->last_query_info;
+ }
+
+ /**
+ * Returns the last query failure information, or false if no failure information was stored.
+ *
+ * @since 5.8.0
+ *
+ * @return bool|array
+ */
+ public function get_last_query_failure_info() {
+ return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
+ }
+
+ /**
+ * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
+ * developers to disable filters supplied by the search widget. Useful if filters are
+ * being defined at the code level.
+ *
+ * @since 5.7.0
+ * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() directly.
+ *
+ * @return bool
+ */
+ public function are_filters_by_widget_disabled() {
+ return Jetpack_Search_Helpers::are_filters_by_widget_disabled();
+ }
+
+ /**
+ * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
+ * and applies those filters to this Jetpack_Search object.
+ *
+ * @since 5.7.0
+ */
+ public function set_filters_from_widgets() {
+ if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) {
+ return;
+ }
+
+ $filters = Jetpack_Search_Helpers::get_filters_from_widgets();
+
+ if ( ! empty( $filters ) ) {
+ $this->set_filters( $filters );
+ }
+ }
+
+ /**
+ * Restricts search results to certain post types via a GET argument.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_Query $query A WP_Query instance.
+ */
+ public function maybe_add_post_type_as_var( WP_Query $query ) {
+ if ( $this->should_handle_query( $query ) && ! empty( $_GET['post_type'] ) ) {
+ $post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
+ ? $post_type = explode( ',', $_GET['post_type'] )
+ : (array) $_GET['post_type'];
+ $post_types = array_map( 'sanitize_key', $post_types );
+ $query->set( 'post_type', $post_types );
+ }
+ }
+
+ /*
+ * Run a search on the WordPress.com public API.
+ *
+ * @since 5.0.0
+ *
+ * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint.
+ *
+ * @return object|WP_Error The response from the public API, or a WP_Error.
+ */
+ public function search( array $es_args ) {
+ $endpoint = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
+ $service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
+
+ $do_authenticated_request = false;
+
+ if ( class_exists( 'Jetpack_Client' ) &&
+ isset( $es_args['authenticated_request'] ) &&
+ true === $es_args['authenticated_request'] ) {
+ $do_authenticated_request = true;
+ }
+
+ unset( $es_args['authenticated_request'] );
+
+ $request_args = array(
+ 'headers' => array(
+ 'Content-Type' => 'application/json',
+ ),
+ 'timeout' => 10,
+ 'user-agent' => 'jetpack_search',
+ );
+
+ $request_body = wp_json_encode( $es_args );
+
+ $start_time = microtime( true );
+
+ if ( $do_authenticated_request ) {
+ $request_args['method'] = 'POST';
+
+ $request = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
+ } else {
+ $request_args = array_merge( $request_args, array(
+ 'body' => $request_body,
+ ) );
+
+ $request = wp_remote_post( $service_url, $request_args );
+ }
+
+ $end_time = microtime( true );
+
+ if ( is_wp_error( $request ) ) {
+ return $request;
+ }
+
+ $response_code = wp_remote_retrieve_response_code( $request );
+
+ $response = json_decode( wp_remote_retrieve_body( $request ), true );
+
+ $took = is_array( $response ) && ! empty( $response['took'] )
+ ? $response['took']
+ : null;
+
+ $query = array(
+ 'args' => $es_args,
+ 'response' => $response,
+ 'response_code' => $response_code,
+ 'elapsed_time' => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
+ 'es_time' => $took,
+ 'url' => $service_url,
+ );
+
+ /**
+ * Fires after a search request has been performed.
+ *
+ * Includes the following info in the $query parameter:
+ *
+ * array args Array of Elasticsearch arguments for the search
+ * array response Raw API response, JSON decoded
+ * int response_code HTTP response code of the request
+ * float elapsed_time Roundtrip time of the search request, in milliseconds
+ * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
+ * string url API url that was queried
+ *
+ * @module search
+ *
+ * @since 5.0.0
+ * @since 5.8.0 This action now fires on all queries instead of just successful queries.
+ *
+ * @param array $query Array of information about the query performed
+ */
+ do_action( 'did_jetpack_search_query', $query );
+
+ if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
+ /**
+ * Fires after a search query request has failed
+ *
+ * @module search
+ *
+ * @since 5.6.0
+ *
+ * @param array Array containing the response code and response from the failed search query
+ */
+ do_action( 'failed_jetpack_search_query', array(
+ 'response_code' => $response_code,
+ 'json' => $response,
+ ) );
+
+ return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Bypass the normal Search query and offload it to Jetpack servers.
+ *
+ * This is the main hook of the plugin and is responsible for returning the posts that match the search query.
+ *
+ * @since 5.0.0
+ *
+ * @param array $posts Current array of posts (still pre-query).
+ * @param WP_Query $query The WP_Query being filtered.
+ *
+ * @return array Array of matching posts.
+ */
+ public function filter__posts_pre_query( $posts, $query ) {
+ if ( ! $this->should_handle_query( $query ) ) {
+ return $posts;
+ }
+
+ $this->do_search( $query );
+
+ if ( ! is_array( $this->search_result ) ) {
+ return $posts;
+ }
+
+ // If no results, nothing to do
+ if ( ! count( $this->search_result['results']['hits'] ) ) {
+ return array();
+ }
+
+ $post_ids = array();
+
+ foreach ( $this->search_result['results']['hits'] as $result ) {
+ $post_ids[] = (int) $result['fields']['post_id'];
+ }
+
+ // Query all posts now
+ $args = array(
+ 'post__in' => $post_ids,
+ 'orderby' => 'post__in',
+ 'perm' => 'readable',
+ 'post_type' => 'any',
+ 'ignore_sticky_posts' => true,
+ 'suppress_filters' => true,
+ );
+
+ $posts_query = new WP_Query( $args );
+
+ // WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to do these manually.
+ $query->found_posts = $this->found_posts;
+ $query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
+
+ return $posts_query->posts;
+ }
+
+ /**
+ * Build up the search, then run it against the Jetpack servers.
+ *
+ * @since 5.0.0
+ *
+ * @param WP_Query $query The original WP_Query to use for the parameters of our search.
+ */
+ public function do_search( WP_Query $query ) {
+ if ( ! $this->should_handle_query( $query ) ) {
+ return;
+ }
+
+ $page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
+
+ // Get maximum allowed offset and posts per page values for the API.
+ $max_offset = Jetpack_Search_Helpers::get_max_offset();
+ $max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
+
+ $posts_per_page = $query->get( 'posts_per_page' );
+ if ( $posts_per_page > $max_posts_per_page ) {
+ $posts_per_page = $max_posts_per_page;
+ }
+
+ // Start building the WP-style search query args.
+ // They'll be translated to ES format args later.
+ $es_wp_query_args = array(
+ 'query' => $query->get( 's' ),
+ 'posts_per_page' => $posts_per_page,
+ 'paged' => $page,
+ 'orderby' => $query->get( 'orderby' ),
+ 'order' => $query->get( 'order' ),
+ );
+
+ if ( ! empty( $this->aggregations ) ) {
+ $es_wp_query_args['aggregations'] = $this->aggregations;
+ }
+
+ // Did we query for authors?
+ if ( $query->get( 'author_name' ) ) {
+ $es_wp_query_args['author_name'] = $query->get( 'author_name' );
+ }
+
+ $es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
+ $es_wp_query_args['terms'] = $this->get_es_wp_query_terms_for_query( $query );
+
+ /**
+ * Modify the search query parameters, such as controlling the post_type.
+ *
+ * These arguments are in the format of WP_Query arguments
+ *
+ * @module search
+ *
+ * @since 5.0.0
+ *
+ * @param array $es_wp_query_args The current query args, in WP_Query format.
+ * @param WP_Query $query The original WP_Query object.
+ */
+ $es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
+
+ // If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
+ // capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
+ if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
+ $query->set_404();
+
+ return;
+ }
+
+ // If there were no post types returned, then 404 to avoid querying against non-public post types, which could
+ // happen if we don't add the post type restriction to the ES query.
+ if ( empty( $es_wp_query_args['post_type'] ) ) {
+ $query->set_404();
+
+ return;
+ }
+
+ // Convert the WP-style args into ES args.
+ $es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
+
+ //Only trust ES to give us IDs, not the content since it is a mirror
+ $es_query_args['fields'] = array(
+ 'post_id',
+ );
+
+ /**
+ * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
+ *
+ * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
+ *
+ * @module search
+ *
+ * @since 5.0.0
+ *
+ * @param array $es_query_args The raw Elasticsearch query args.
+ * @param WP_Query $query The original WP_Query object.
+ */
+ $es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
+
+ // Do the actual search query!
+ $this->search_result = $this->search( $es_query_args );
+
+ if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
+ $this->found_posts = 0;
+
+ return;
+ }
+
+ // If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
+ if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
+ $this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
+ }
+
+ // Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
+ $this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
+ }
+
+ /**
+ * If the query has already been run before filters have been updated, then we need to re-run the query
+ * to get the latest aggregations.
+ *
+ * This is especially useful for supporting widget management in the customizer.
+ *
+ * @since 5.8.0
+ *
+ * @return bool Whether the query was successful or not.
+ */
+ public function update_search_results_aggregations() {
+ if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
+ return false;
+ }
+
+ $es_args = $this->last_query_info['args'];
+ $builder = new Jetpack_WPES_Query_Builder();
+ $this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
+ $es_args['aggregations'] = $builder->build_aggregation();
+
+ $this->search_result = $this->search( $es_args );
+
+ return ! is_wp_error( $this->search_result );
+ }
+
+ /**
+ * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search.
+ *
+ * @since 5.0.0
+ *
+ * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query.
+ *
+ * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments).
+ */
+ public function get_es_wp_query_terms_for_query( WP_Query $query ) {
+ $args = array();
+
+ $the_tax_query = $query->tax_query;
+
+ if ( ! $the_tax_query ) {
+ return $args;
+ }
+
+
+ if ( ! $the_tax_query instanceof WP_Tax_Query || empty( $the_tax_query->queried_terms ) || ! is_array( $the_tax_query->queried_terms ) ) {
+ return $args;
+ }
+
+ $args = array();
+
+ foreach ( $the_tax_query->queries as $tax_query ) {
+ // Right now we only support slugs...see note above
+ if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
+ continue;
+ }
+
+ $taxonomy = $tax_query['taxonomy'];
+
+ if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
+ $args[ $taxonomy ] = array();
+ }
+
+ $args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
+ }
+
+ return $args;
+ }
+
+ /**
+ * Parse out the post type from a WP_Query.
+ *
+ * Only allows post types that are not marked as 'exclude_from_search'.
+ *
+ * @since 5.0.0
+ *
+ * @param WP_Query $query Original WP_Query object.
+ *
+ * @return array Array of searchable post types corresponding to the original query.
+ */
+ public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
+ $post_types = $query->get( 'post_type' );
+
+ // If we're searching 'any', we want to only pass searchable post types to Elasticsearch.
+ if ( 'any' === $post_types ) {
+ $post_types = array_values( get_post_types( array(
+ 'exclude_from_search' => false,
+ ) ) );
+ }
+
+ if ( ! is_array( $post_types ) ) {
+ $post_types = array( $post_types );
+ }
+
+ $post_types = array_unique( $post_types );
+
+ $sanitized_post_types = array();
+
+ // Make sure the post types are queryable.
+ foreach ( $post_types as $post_type ) {
+ if ( ! $post_type ) {
+ continue;
+ }
+
+ $post_type_object = get_post_type_object( $post_type );
+ if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
+ continue;
+ }
+
+ $sanitized_post_types[] = $post_type;
+ }
+
+ return $sanitized_post_types;
+ }
+
+ /**
+ * Get the Elasticsearch result.
+ *
+ * @since 5.0.0
+ *
+ * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response.
+ *
+ * @return array|bool The search results, or false if there was a failure.
+ */
+ public function get_search_result( $raw = false ) {
+ if ( $raw ) {
+ return $this->search_result;
+ }
+
+ return ( ! empty( $this->search_result ) && ! is_wp_error( $this->search_result ) && is_array( $this->search_result ) && ! empty( $this->search_result['results'] ) ) ? $this->search_result['results'] : false;
+ }
+
+ /**
+ * Add the date portion of a WP_Query onto the query args.
+ *
+ * @since 5.0.0
+ *
+ * @param array $es_wp_query_args The Elasticsearch query arguments in WordPress form.
+ * @param WP_Query $query The original WP_Query.
+ *
+ * @return array The es wp query args, with date filters added (as needed).
+ */
+ public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
+ if ( $query->get( 'year' ) ) {
+ if ( $query->get( 'monthnum' ) ) {
+ // Padding
+ $date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
+
+ if ( $query->get( 'day' ) ) {
+ // Padding
+ $date_day = sprintf( '%02d', $query->get( 'day' ) );
+
+ $date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
+ $date_end = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
+ } else {
+ $days_in_month = date( 't', mktime( 0, 0, 0, $query->get( 'monthnum' ), 14, $query->get( 'year' ) ) ); // 14 = middle of the month so no chance of DST issues
+
+ $date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
+ $date_end = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
+ }
+ } else {
+ $date_start = $query->get( 'year' ) . '-01-01 00:00:00';
+ $date_end = $query->get( 'year' ) . '-12-31 23:59:59';
+ }
+
+ $es_wp_query_args['date_range'] = array(
+ 'field' => 'date',
+ 'gte' => $date_start,
+ 'lte' => $date_end,
+ );
+ }
+
+ return $es_wp_query_args;
+ }
+
+ /**
+ * Converts WP_Query style args to Elasticsearch args.
+ *
+ * @since 5.0.0
+ *
+ * @param array $args Array of WP_Query style arguments.
+ *
+ * @return array Array of ES style query arguments.
+ */
+ public function convert_wp_es_to_es_args( array $args ) {
+ jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
+
+ $defaults = array(
+ 'blog_id' => get_current_blog_id(),
+ 'query' => null, // Search phrase
+ 'query_fields' => array(), // list of fields to search
+ 'excess_boost' => array(), // map of field to excess boost values (multiply)
+ 'post_type' => null, // string or an array
+ 'terms' => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
+ 'author' => null, // id or an array of ids
+ 'author_name' => array(), // string or an array
+ 'date_range' => null, // array( 'field' => 'date', 'gt' => 'YYYY-MM-dd', 'lte' => 'YYYY-MM-dd' ); date formats: 'YYYY-MM-dd' or 'YYYY-MM-dd HH:MM:SS'
+ 'orderby' => null, // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
+ 'order' => 'DESC',
+ 'posts_per_page' => 10,
+ 'offset' => null,
+ 'paged' => null,
+ /**
+ * Aggregations. Examples:
+ * array(
+ * 'Tag' => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
+ * 'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
+ * );
+ */
+ 'aggregations' => null,
+ );
+
+ $args = wp_parse_args( $args, $defaults );
+
+ $parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) );
+
+ if ( empty( $args['query_fields'] ) ) {
+ if ( defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX ) {
+ // VIP indices do not have per language fields
+ $match_fields = $this->_get_caret_boosted_fields(
+ array(
+ 'title' => 0.1,
+ 'content' => 0.1,
+ 'excerpt' => 0.1,
+ 'tag.name' => 0.1,
+ 'category.name' => 0.1,
+ 'author_login' => 0.1,
+ 'author' => 0.1,
+ )
+ );
+
+ $boost_fields = $this->_get_caret_boosted_fields(
+ $this->_apply_boosts_multiplier( array(
+ 'title' => 2,
+ 'tag.name' => 1,
+ 'category.name' => 1,
+ 'author_login' => 1,
+ 'author' => 1,
+ ), $args['excess_boost'] )
+ );
+
+ $boost_phrase_fields = $this->_get_caret_boosted_fields(
+ array(
+ 'title' => 1,
+ 'content' => 1,
+ 'excerpt' => 1,
+ 'tag.name' => 1,
+ 'category.name' => 1,
+ 'author' => 1,
+ )
+ );
+ } else {
+ $match_fields = $parser->merge_ml_fields(
+ array(
+ 'title' => 0.1,
+ 'content' => 0.1,
+ 'excerpt' => 0.1,
+ 'tag.name' => 0.1,
+ 'category.name' => 0.1,
+ ),
+ $this->_get_caret_boosted_fields( array(
+ 'author_login' => 0.1,
+ 'author' => 0.1,
+ ) )
+ );
+
+ $boost_fields = $parser->merge_ml_fields(
+ $this->_apply_boosts_multiplier( array(
+ 'title' => 2,
+ 'tag.name' => 1,
+ 'category.name' => 1,
+ ), $args['excess_boost'] ),
+ $this->_get_caret_boosted_fields( $this->_apply_boosts_multiplier( array(
+ 'author_login' => 1,
+ 'author' => 1,
+ ), $args['excess_boost'] ) )
+ );
+
+ $boost_phrase_fields = $parser->merge_ml_fields(
+ array(
+ 'title' => 1,
+ 'content' => 1,
+ 'excerpt' => 1,
+ 'tag.name' => 1,
+ 'category.name' => 1,
+ ),
+ $this->_get_caret_boosted_fields( array(
+ 'author' => 1,
+ ) )
+ );
+ }
+ } else {
+ // If code is overriding the fields, then use that. Important for backwards compatibility.
+ $match_fields = $args['query_fields'];
+ $boost_phrase_fields = $match_fields;
+ $boost_fields = null;
+ }
+
+ $parser->phrase_filter( array(
+ 'must_query_fields' => $match_fields,
+ 'boost_query_fields' => null,
+ ) );
+ $parser->remaining_query( array(
+ 'must_query_fields' => $match_fields,
+ 'boost_query_fields' => $boost_fields,
+ ) );
+
+ // Boost on phrase matches
+ $parser->remaining_query( array(
+ 'boost_query_fields' => $boost_phrase_fields,
+ 'boost_query_type' => 'phrase',
+ ) );
+
+ /**
+ * Modify the recency decay parameters for the search query.
+ *
+ * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments:
+ * - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
+ * - offset: Number of days/months/years (eg 30d). All posts within this time range of the origin (before and after) will have no decay applied. Default is no offset.
+ * - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d
+ * - decay: The amount of decay applied at offset+scale. Default 0.9.
+ *
+ * The curve applied is a Gaussian. More details available at {@see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay}
+ *
+ * @module search
+ *
+ * @since 5.8.0
+ *
+ * @param array $decay_params The decay parameters.
+ * @param array $args The WP query parameters.
+ */
+ $decay_params = apply_filters(
+ 'jetpack_search_recency_score_decay',
+ array(
+ 'origin' => date( 'Y-m-d' ),
+ 'scale' => '360d',
+ 'decay' => 0.9,
+ ),
+ $args
+ );
+
+ if ( ! empty( $decay_params ) ) {
+ // Newer content gets weighted slightly higher
+ $parser->add_decay( 'gauss', array(
+ 'date_gmt' => $decay_params
+ ) );
+ }
+
+ $es_query_args = array(
+ 'blog_id' => absint( $args['blog_id'] ),
+ 'size' => absint( $args['posts_per_page'] ),
+ );
+
+ // ES "from" arg (offset)
+ if ( $args['offset'] ) {
+ $es_query_args['from'] = absint( $args['offset'] );
+ } elseif ( $args['paged'] ) {
+ $es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
+ }
+
+ $es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
+
+ if ( ! is_array( $args['author_name'] ) ) {
+ $args['author_name'] = array( $args['author_name'] );
+ }
+
+ // ES stores usernames, not IDs, so transform
+ if ( ! empty( $args['author'] ) ) {
+ if ( ! is_array( $args['author'] ) ) {
+ $args['author'] = array( $args['author'] );
+ }
+
+ foreach ( $args['author'] as $author ) {
+ $user = get_user_by( 'id', $author );
+
+ if ( $user && ! empty( $user->user_login ) ) {
+ $args['author_name'][] = $user->user_login;
+ }
+ }
+ }
+
+ //////////////////////////////////////////////////
+ // Build the filters from the query elements.
+ // Filters rock because they are cached from one query to the next
+ // but they are cached as individual filters, rather than all combined together.
+ // May get performance boost by also caching the top level boolean filter too.
+
+ if ( $args['post_type'] ) {
+ if ( ! is_array( $args['post_type'] ) ) {
+ $args['post_type'] = array( $args['post_type'] );
+ }
+
+ $parser->add_filter( array(
+ 'terms' => array(
+ 'post_type' => $args['post_type'],
+ ),
+ ) );
+ }
+
+ if ( $args['author_name'] ) {
+ $parser->add_filter( array(
+ 'terms' => array(
+ 'author_login' => $args['author_name'],
+ ),
+ ) );
+ }
+
+ if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
+ $field = $args['date_range']['field'];
+
+ unset( $args['date_range']['field'] );
+
+ $parser->add_filter( array(
+ 'range' => array(
+ $field => $args['date_range'],
+ ),
+ ) );
+ }
+
+ if ( is_array( $args['terms'] ) ) {
+ foreach ( $args['terms'] as $tax => $terms ) {
+ $terms = (array) $terms;
+
+ if ( count( $terms ) && mb_strlen( $tax ) ) {
+ switch ( $tax ) {
+ case 'post_tag':
+ $tax_fld = 'tag.slug';
+
+ break;
+
+ case 'category':
+ $tax_fld = 'category.slug';
+
+ break;
+
+ default:
+ $tax_fld = 'taxonomy.' . $tax . '.slug';
+
+ break;
+ }
+
+ foreach ( $terms as $term ) {
+ $parser->add_filter( array(
+ 'term' => array(
+ $tax_fld => $term,
+ ),
+ ) );
+ }
+ }
+ }
+ }
+
+ if ( ! $args['orderby'] ) {
+ if ( $args['query'] ) {
+ $args['orderby'] = array( 'relevance' );
+ } else {
+ $args['orderby'] = array( 'date' );
+ }
+ }
+
+ // Validate the "order" field
+ switch ( strtolower( $args['order'] ) ) {
+ case 'asc':
+ $args['order'] = 'asc';
+ break;
+
+ case 'desc':
+ default:
+ $args['order'] = 'desc';
+ break;
+ }
+
+ $es_query_args['sort'] = array();
+
+ foreach ( (array) $args['orderby'] as $orderby ) {
+ // Translate orderby from WP field to ES field
+ switch ( $orderby ) {
+ case 'relevance' :
+ //never order by score ascending
+ $es_query_args['sort'][] = array(
+ '_score' => array(
+ 'order' => 'desc',
+ ),
+ );
+
+ break;
+
+ case 'date' :
+ $es_query_args['sort'][] = array(
+ 'date' => array(
+ 'order' => $args['order'],
+ ),
+ );
+
+ break;
+
+ case 'ID' :
+ $es_query_args['sort'][] = array(
+ 'id' => array(
+ 'order' => $args['order'],
+ ),
+ );
+
+ break;
+
+ case 'author' :
+ $es_query_args['sort'][] = array(
+ 'author.raw' => array(
+ 'order' => $args['order'],
+ ),
+ );
+
+ break;
+ } // End switch().
+ } // End foreach().
+
+ if ( empty( $es_query_args['sort'] ) ) {
+ unset( $es_query_args['sort'] );
+ }
+
+ // Aggregations
+ if ( ! empty( $args['aggregations'] ) ) {
+ $this->add_aggregations_to_es_query_builder( $args['aggregations'], $parser );
+ }
+
+ $es_query_args['filter'] = $parser->build_filter();
+ $es_query_args['query'] = $parser->build_query();
+ $es_query_args['aggregations'] = $parser->build_aggregation();
+
+ return $es_query_args;
+ }
+
+ /**
+ * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
+ *
+ * @since 5.0.0
+ *
+ * @param array $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder.
+ * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query.
+ */
+ public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
+ foreach ( $aggregations as $label => $aggregation ) {
+ switch ( $aggregation['type'] ) {
+ case 'taxonomy':
+ $this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
+
+ break;
+
+ case 'post_type':
+ $this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
+
+ break;
+
+ case 'date_histogram':
+ $this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
+ *
+ * @since 5.0.0
+ *
+ * @param array $aggregation The aggregation to add to the query builder.
+ * @param string $label The 'label' (unique id) for this aggregation.
+ * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query.
+ */
+ public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
+ $field = null;
+
+ switch ( $aggregation['taxonomy'] ) {
+ case 'post_tag':
+ $field = 'tag';
+ break;
+
+ case 'category':
+ $field = 'category';
+ break;
+
+ default:
+ $field = 'taxonomy.' . $aggregation['taxonomy'];
+ break;
+ }
+
+ $builder->add_aggs( $label, array(
+ 'terms' => array(
+ 'field' => $field . '.slug',
+ 'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
+ ),
+ ) );
+ }
+
+ /**
+ * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
+ *
+ * @since 5.0.0
+ *
+ * @param array $aggregation The aggregation to add to the query builder.
+ * @param string $label The 'label' (unique id) for this aggregation.
+ * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query.
+ */
+ public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
+ $builder->add_aggs( $label, array(
+ 'terms' => array(
+ 'field' => 'post_type',
+ 'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
+ ),
+ ) );
+ }
+
+ /**
+ * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
+ *
+ * @since 5.0.0
+ *
+ * @param array $aggregation The aggregation to add to the query builder.
+ * @param string $label The 'label' (unique id) for this aggregation.
+ * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query.
+ */
+ public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
+ $args = array(
+ 'interval' => $aggregation['interval'],
+ 'field' => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
+ );
+
+ if ( isset( $aggregation['min_doc_count'] ) ) {
+ $args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
+ } else {
+ $args['min_doc_count'] = 1;
+ }
+
+ $builder->add_aggs( $label, array(
+ 'date_histogram' => $args,
+ ) );
+ }
+
+ /**
+ * And an existing filter object with a list of additional filters.
+ *
+ * Attempts to optimize the filters somewhat.
+ *
+ * @since 5.0.0
+ *
+ * @param array $curr_filter The existing filters to build upon.
+ * @param array $filters The new filters to add.
+ *
+ * @return array The resulting merged filters.
+ */
+ public static function and_es_filters( array $curr_filter, array $filters ) {
+ if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
+ if ( 1 === count( $filters ) ) {
+ return $filters[0];
+ }
+
+ return array(
+ 'and' => $filters,
+ );
+ }
+
+ return array(
+ 'and' => array_merge( array( $curr_filter ), $filters ),
+ );
+ }
+
+ /**
+ * Set the available filters for the search.
+ *
+ * These get rendered via the Jetpack_Search_Widget() widget.
+ *
+ * Behind the scenes, these are implemented using Elasticsearch Aggregations.
+ *
+ * If you do not require counts of how many documents match each filter, please consider using regular WP Query
+ * arguments instead, such as via the jetpack_search_es_wp_query_args filter
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
+ *
+ * @since 5.0.0
+ *
+ * @param array $aggregations Array of filters (aggregations) to apply to the search
+ */
+ public function set_filters( array $aggregations ) {
+ foreach ( (array) $aggregations as $key => $agg ) {
+ if ( empty( $agg['name'] ) ) {
+ $aggregations[ $key ]['name'] = $key;
+ }
+ }
+ $this->aggregations = $aggregations;
+ }
+
+ /**
+ * Set the search's facets (deprecated).
+ *
+ * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead.
+ *
+ * @see Jetpack_Search::set_filters()
+ *
+ * @param array $facets Array of facets to apply to the search.
+ */
+ public function set_facets( array $facets ) {
+ _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
+
+ $this->set_filters( $facets );
+ }
+
+ /**
+ * Get the raw Aggregation results from the Elasticsearch response.
+ *
+ * @since 5.0.0
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
+ *
+ * @return array Array of Aggregations performed on the search.
+ */
+ public function get_search_aggregations_results() {
+ $aggregations = array();
+
+ $search_result = $this->get_search_result();
+
+ if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
+ $aggregations = $search_result['aggregations'];
+ }
+
+ return $aggregations;
+ }
+
+ /**
+ * Get the raw Facet results from the Elasticsearch response.
+ *
+ * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead.
+ *
+ * @see Jetpack_Search::get_search_aggregations_results()
+ *
+ * @return array Array of Facets performed on the search.
+ */
+ public function get_search_facets() {
+ _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
+
+ return $this->get_search_aggregations_results();
+ }
+
+ /**
+ * Get the results of the Filters performed, including the number of matching documents.
+ *
+ * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
+ * matching buckets, the url for applying/removing each bucket, etc.
+ *
+ * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
+ * member if you need to access the raw filters set in Jetpack_Search::set_filters().
+ *
+ * @since 5.0.0
+ *
+ * @param WP_Query $query The optional original WP_Query to use for determining which filters are active. Defaults to the main query.
+ *
+ * @return array Array of filters applied and info about them.
+ */
+ public function get_filters( WP_Query $query = null ) {
+ if ( ! $query instanceof WP_Query ) {
+ global $wp_query;
+
+ $query = $wp_query;
+ }
+
+ $aggregation_data = $this->aggregations;
+
+ if ( empty( $aggregation_data ) ) {
+ return $aggregation_data;
+ }
+
+ $aggregation_results = $this->get_search_aggregations_results();
+
+ if ( ! $aggregation_results ) {
+ return $aggregation_data;
+ }
+
+ // NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
+ foreach ( $aggregation_results as $label => $aggregation ) {
+ if ( empty( $aggregation ) ) {
+ continue;
+ }
+
+ $type = $this->aggregations[ $label ]['type'];
+
+ $aggregation_data[ $label ]['buckets'] = array();
+
+ $existing_term_slugs = array();
+
+ $tax_query_var = null;
+
+ // Figure out which terms are active in the query, for this taxonomy
+ if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
+ $tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] );
+
+ if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
+ foreach ( $query->tax_query->queries as $tax_query ) {
+ if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
+ 'slug' === $tax_query['field'] &&
+ is_array( $tax_query['terms'] ) ) {
+ $existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
+ }
+ }
+ }
+ }
+
+ // Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc.
+ $buckets = array();
+
+ if ( ! empty( $aggregation['buckets'] ) ) {
+ $buckets = (array) $aggregation['buckets'];
+ }
+
+ if ( 'date_histogram' == $type ) {
+ //re-order newest to oldest
+ $buckets = array_reverse( $buckets );
+ }
+
+ // Some aggregation types like date_histogram don't support the max results parameter
+ if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
+ $buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
+ }
+
+ foreach ( $buckets as $item ) {
+ $query_vars = array();
+ $active = false;
+ $remove_url = null;
+ $name = '';
+
+ // What type was the original aggregation?
+ switch ( $type ) {
+ case 'taxonomy':
+ $taxonomy = $this->aggregations[ $label ]['taxonomy'];
+
+ $term = get_term_by( 'slug', $item['key'], $taxonomy );
+
+ if ( ! $term || ! $tax_query_var ) {
+ continue 2; // switch() is considered a looping structure
+ }
+
+ $query_vars = array(
+ $tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
+ );
+
+ $name = $term->name;
+
+ // Let's determine if this term is active or not
+
+ if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
+ $active = true;
+
+ $slug_count = count( $existing_term_slugs );
+
+ if ( $slug_count > 1 ) {
+ $remove_url = Jetpack_Search_Helpers::add_query_arg(
+ $tax_query_var,
+ rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
+ );
+ } else {
+ $remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var );
+ }
+ }
+
+ break;
+
+ case 'post_type':
+ $post_type = get_post_type_object( $item['key'] );
+
+ if ( ! $post_type || $post_type->exclude_from_search ) {
+ continue 2; // switch() is considered a looping structure
+ }
+
+ $query_vars = array(
+ 'post_type' => $item['key'],
+ );
+
+ $name = $post_type->labels->singular_name;
+
+ // Is this post type active on this search?
+ $post_types = $query->get( 'post_type' );
+
+ if ( ! is_array( $post_types ) ) {
+ $post_types = array( $post_types );
+ }
+
+ if ( in_array( $item['key'], $post_types ) ) {
+ $active = true;
+
+ $post_type_count = count( $post_types );
+
+ // For the right 'remove filter' url, we need to remove the post type from the array, or remove the param entirely if it's the only one
+ if ( $post_type_count > 1 ) {
+ $remove_url = Jetpack_Search_Helpers::add_query_arg(
+ 'post_type',
+ rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) )
+ );
+ } else {
+ $remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
+ }
+ }
+
+ break;
+
+ case 'date_histogram':
+ $timestamp = $item['key'] / 1000;
+
+ $current_year = $query->get( 'year' );
+ $current_month = $query->get( 'monthnum' );
+ $current_day = $query->get( 'day' );
+
+ switch ( $this->aggregations[ $label ]['interval'] ) {
+ case 'year':
+ $year = (int) date( 'Y', $timestamp );
+
+ $query_vars = array(
+ 'year' => $year,
+ 'monthnum' => false,
+ 'day' => false,
+ );
+
+ $name = $year;
+
+ // Is this year currently selected?
+ if ( ! empty( $current_year ) && (int) $current_year === $year ) {
+ $active = true;
+
+ $remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
+ }
+
+ break;
+
+ case 'month':
+ $year = (int) date( 'Y', $timestamp );
+ $month = (int) date( 'n', $timestamp );
+
+ $query_vars = array(
+ 'year' => $year,
+ 'monthnum' => $month,
+ 'day' => false,
+ );
+
+ $name = date( 'F Y', $timestamp );
+
+ // Is this month currently selected?
+ if ( ! empty( $current_year ) && (int) $current_year === $year &&
+ ! empty( $current_month ) && (int) $current_month === $month ) {
+ $active = true;
+
+ $remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
+ }
+
+ break;
+
+ case 'day':
+ $year = (int) date( 'Y', $timestamp );
+ $month = (int) date( 'n', $timestamp );
+ $day = (int) date( 'j', $timestamp );
+
+ $query_vars = array(
+ 'year' => $year,
+ 'monthnum' => $month,
+ 'day' => $day,
+ );
+
+ $name = date( 'F jS, Y', $timestamp );
+
+ // Is this day currently selected?
+ if ( ! empty( $current_year ) && (int) $current_year === $year &&
+ ! empty( $current_month ) && (int) $current_month === $month &&
+ ! empty( $current_day ) && (int) $current_day === $day ) {
+ $active = true;
+
+ $remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
+ }
+
+ break;
+
+ default:
+ continue 3; // switch() is considered a looping structure
+ } // End switch().
+
+ break;
+
+ default:
+ //continue 2; // switch() is considered a looping structure
+ } // End switch().
+
+ // Need to urlencode param values since add_query_arg doesn't
+ $url_params = urlencode_deep( $query_vars );
+
+ $aggregation_data[ $label ]['buckets'][] = array(
+ 'url' => Jetpack_Search_Helpers::add_query_arg( $url_params ),
+ 'query_vars' => $query_vars,
+ 'name' => $name,
+ 'count' => $item['doc_count'],
+ 'active' => $active,
+ 'remove_url' => $remove_url,
+ 'type' => $type,
+ 'type_label' => $aggregation_data[ $label ]['name'],
+ 'widget_id' => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
+ );
+ } // End foreach().
+ } // End foreach().
+
+ /**
+ * Modify the aggregation filters returned by get_filters().
+ *
+ * Useful if you are setting custom filters outside of the supported filters (taxonomy, post_type etc.) and
+ * want to hook them up so they're returned when you call `get_filters()`.
+ *
+ * @module search
+ *
+ * @since 6.9.0
+ *
+ * @param array $aggregation_data The array of filters keyed on label.
+ * @param WP_Query $query The WP_Query object.
+ */
+ return apply_filters( 'jetpack_search_get_filters', $aggregation_data, $query );
+ }
+
+ /**
+ * Get the results of the facets performed.
+ *
+ * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead.
+ *
+ * @see Jetpack_Search::get_filters()
+ *
+ * @return array $facets Array of facets applied and info about them.
+ */
+ public function get_search_facet_data() {
+ _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
+
+ return $this->get_filters();
+ }
+
+ /**
+ * Get the filters that are currently applied to this search.
+ *
+ * @since 5.0.0
+ *
+ * @return array Array of filters that were applied.
+ */
+ public function get_active_filter_buckets() {
+ $active_buckets = array();
+
+ $filters = $this->get_filters();
+
+ if ( ! is_array( $filters ) ) {
+ return $active_buckets;
+ }
+
+ foreach ( $filters as $filter ) {
+ if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
+ foreach ( $filter['buckets'] as $item ) {
+ if ( isset( $item['active'] ) && $item['active'] ) {
+ $active_buckets[] = $item;
+ }
+ }
+ }
+ }
+
+ return $active_buckets;
+ }
+
+ /**
+ * Get the filters that are currently applied to this search.
+ *
+ * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead.
+ *
+ * @see Jetpack_Search::get_active_filter_buckets()
+ *
+ * @return array Array of filters that were applied.
+ */
+ public function get_current_filters() {
+ _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
+
+ return $this->get_active_filter_buckets();
+ }
+
+ /**
+ * Calculate the right query var to use for a given taxonomy.
+ *
+ * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter.
+ *
+ * @since 5.0.0
+ *
+ * @param string $taxonomy_name The name of the taxonomy for which to get the query var.
+ *
+ * @return bool|string The query var to use for this taxonomy, or false if none found.
+ */
+ public function get_taxonomy_query_var( $taxonomy_name ) {
+ $taxonomy = get_taxonomy( $taxonomy_name );
+
+ if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
+ return false;
+ }
+
+ /**
+ * Modify the query var to use for a given taxonomy
+ *
+ * @module search
+ *
+ * @since 5.0.0
+ *
+ * @param string $query_var The current query_var for the taxonomy
+ * @param string $taxonomy_name The taxonomy name
+ */
+ return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
+ }
+
+ /**
+ * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
+ * which is the input order.
+ *
+ * Necessary because ES does not always return aggregations in the same order that you pass them in,
+ * and it should be possible to control the display order easily.
+ *
+ * @since 5.0.0
+ *
+ * @param array $aggregations Aggregation results to be reordered.
+ * @param array $desired Array with keys representing the desired ordering.
+ *
+ * @return array A new array with reordered keys, matching those in $desired.
+ */
+ public function fix_aggregation_ordering( array $aggregations, array $desired ) {
+ if ( empty( $aggregations ) || empty( $desired ) ) {
+ return $aggregations;
+ }
+
+ $reordered = array();
+
+ foreach ( array_keys( $desired ) as $agg_name ) {
+ if ( isset( $aggregations[ $agg_name ] ) ) {
+ $reordered[ $agg_name ] = $aggregations[ $agg_name ];
+ }
+ }
+
+ return $reordered;
+ }
+
+ /**
+ * Sends events to Tracks when a search filters widget is updated.
+ *
+ * @since 5.8.0
+ *
+ * @param string $option The option name. Only "widget_jetpack-search-filters" is cared about.
+ * @param array $old_value The old option value.
+ * @param array $new_value The new option value.
+ */
+ public function track_widget_updates( $option, $old_value, $new_value ) {
+ if ( 'widget_jetpack-search-filters' !== $option ) {
+ return;
+ }
+
+ $event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
+ if ( ! $event ) {
+ return;
+ }
+
+ jetpack_tracks_record_event(
+ wp_get_current_user(),
+ sprintf( 'jetpack_search_widget_%s', $event['action'] ),
+ $event['widget']
+ );
+ }
+
+ /**
+ * Moves any active search widgets to the inactive category.
+ *
+ * @since 5.9.0
+ *
+ * @param string $module Unused. The Jetpack module being disabled.
+ */
+ public function move_search_widgets_to_inactive( $module ) {
+ if ( ! is_active_widget( false, false, Jetpack_Search_Helpers::FILTER_WIDGET_BASE, true ) ) {
+ return;
+ }
+
+ $sidebars_widgets = wp_get_sidebars_widgets();
+
+ if ( ! is_array( $sidebars_widgets ) ) {
+ return;
+ }
+
+ $changed = false;
+
+ foreach ( $sidebars_widgets as $sidebar => $widgets ) {
+ if ( 'wp_inactive_widgets' === $sidebar || 'orphaned_widgets' === substr( $sidebar, 0, 16 ) ) {
+ continue;
+ }
+
+ if ( is_array( $widgets ) ) {
+ foreach ( $widgets as $key => $widget ) {
+ if ( _get_widget_id_base( $widget ) == Jetpack_Search_Helpers::FILTER_WIDGET_BASE ) {
+ $changed = true;
+
+ array_unshift( $sidebars_widgets['wp_inactive_widgets'], $widget );
+ unset( $sidebars_widgets[ $sidebar ][ $key ] );
+ }
+ }
+ }
+ }
+
+ if ( $changed ) {
+ wp_set_sidebars_widgets( $sidebars_widgets );
+ }
+ }
+
+ /**
+ * Determine whether a given WP_Query should be handled by ElasticSearch.
+ *
+ * @param WP_Query $query The WP_Query object.
+ *
+ * @return bool
+ */
+ public function should_handle_query( $query ) {
+ /**
+ * Determine whether a given WP_Query should be handled by ElasticSearch.
+ *
+ * @module search
+ *
+ * @since 5.6.0
+ *
+ * @param bool $should_handle Should be handled by Jetpack Search.
+ * @param WP_Query $query The WP_Query object.
+ */
+ return apply_filters( 'jetpack_search_should_handle_query', $query->is_main_query() && $query->is_search(), $query );
+ }
+
+ /**
+ * Transforms an array with fields name as keys and boosts as value into
+ * shorthand "caret" format.
+ *
+ * @param array $fields_boost [ "title" => "2", "content" => "1" ]
+ *
+ * @return array [ "title^2", "content^1" ]
+ */
+ private function _get_caret_boosted_fields( array $fields_boost ) {
+ $caret_boosted_fields = array();
+ foreach ( $fields_boost as $field => $boost ) {
+ $caret_boosted_fields[] = "$field^$boost";
+ }
+ return $caret_boosted_fields;
+ }
+
+ /**
+ * Apply a multiplier to boost values.
+ *
+ * @param array $fields_boost [ "title" => 2, "content" => 1 ]
+ * @param array $fields_boost_multiplier [ "title" => 0.1234 ]
+ *
+ * @return array [ "title" => "0.247", "content" => "1.000" ]
+ */
+ private function _apply_boosts_multiplier( array $fields_boost, array $fields_boost_multiplier ) {
+ foreach( $fields_boost as $field_name => $field_boost ) {
+ if ( isset( $fields_boost_multiplier[ $field_name ] ) ) {
+ $fields_boost[ $field_name ] *= $fields_boost_multiplier[ $field_name ];
+ }
+
+ // Set a floor and format the number as string
+ $fields_boost[ $field_name ] = number_format(
+ max( 0.001, $fields_boost[ $field_name ] ),
+ 3, '.', ''
+ );
+ }
+
+ return $fields_boost;
+ }
+}
diff --git a/plugins/jetpack/modules/seo-tools.php b/plugins/jetpack/modules/seo-tools.php
new file mode 100644
index 00000000..f926c5c4
--- /dev/null
+++ b/plugins/jetpack/modules/seo-tools.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Module Name: SEO Tools
+ * Module Description: Better results on search engines and social media.
+ * Sort Order: 35
+ * Recommendation Order: 15
+ * First Introduced: 4.4
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Social, Appearance
+ * Feature: Traffic
+ * Additional Search Queries: search engine optimization, social preview, meta description, custom title format
+ * Plans: business, premium
+ */
+
+include dirname( __FILE__ ) . '/seo-tools/jetpack-seo.php';
+include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
+
+// Suppress SEO Tools output if any of the following plugins is active.
+$jetpack_seo_conflicting_plugins = array(
+ 'wordpress-seo/wp-seo.php',
+ 'wordpress-seo-premium/wp-seo-premium.php',
+ 'all-in-one-seo-pack/all_in_one_seo_pack.php',
+ 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php',
+ 'autodescription/autodescription.php',
+);
+
+foreach( $jetpack_seo_conflicting_plugins as $seo_plugin ) {
+ if ( Jetpack::is_plugin_active( $seo_plugin ) ) {
+ // Disable all custom meta tags that SEO tools manages.
+ add_filter( 'jetpack_disable_seo_tools', '__return_true' );
+
+ // Also disable default meta tags.
+ add_filter( 'jetpack_seo_meta_tags_enabled', '__return_false' );
+ break;
+ }
+}
+
+new Jetpack_SEO;
diff --git a/plugins/jetpack/modules/seo-tools/jetpack-seo-posts.php b/plugins/jetpack/modules/seo-tools/jetpack-seo-posts.php
new file mode 100644
index 00000000..677f6c8b
--- /dev/null
+++ b/plugins/jetpack/modules/seo-tools/jetpack-seo-posts.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * Class containing utility static methods for managing SEO custom descriptions for Posts and Pages.
+ */
+class Jetpack_SEO_Posts {
+ /**
+ * Key of the post meta value that will be used to store post custom description.
+ */
+ const DESCRIPTION_META_KEY = 'advanced_seo_description';
+
+ /**
+ * Build meta description for post SEO.
+ *
+ * @param WP_Post $post Source of data for custom description.
+ *
+ * @return string Post description or empty string.
+ */
+ public static function get_post_description( $post ) {
+ if ( empty( $post ) ) {
+ return '';
+ }
+
+ if ( post_password_required() || ! is_singular() ) {
+ return '';
+ }
+
+ // Business users can overwrite the description
+ $custom_description = self::get_post_custom_description( $post );
+
+ if ( ! empty( $custom_description ) ) {
+ return $custom_description;
+ }
+
+ if ( ! empty( $post->post_excerpt ) ) {
+ return $post->post_excerpt;
+ }
+
+ return $post->post_content;
+ }
+
+ /**
+ * Returns post's custom meta description if it is set, and if
+ * SEO tools are enabled for current blog.
+ *
+ * @param WP_Post $post Source of data for custom description
+ *
+ * @return string Custom description or empty string
+ */
+ public static function get_post_custom_description( $post ) {
+ if ( empty( $post ) ) {
+ return '';
+ }
+
+ $custom_description = get_post_meta( $post->ID, self::DESCRIPTION_META_KEY, true );
+
+ if ( empty( $custom_description ) || ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
+ return '';
+ }
+
+ return $custom_description;
+ }
+}
diff --git a/plugins/jetpack/modules/seo-tools/jetpack-seo-titles.php b/plugins/jetpack/modules/seo-tools/jetpack-seo-titles.php
new file mode 100644
index 00000000..a1bac401
--- /dev/null
+++ b/plugins/jetpack/modules/seo-tools/jetpack-seo-titles.php
@@ -0,0 +1,301 @@
+<?php
+
+/*
+ * Each title format is an array of arrays containing two values:
+ * - type
+ * - value
+ *
+ * Possible values for type are: 'token' and 'string'.
+ * Possible values for 'value' are: any string in case that 'type' is set
+ * to 'string', or allowed token values for page type in case that 'type'
+ * is set to 'token'.
+ *
+ * Examples of valid formats:
+ *
+ * [
+ * 'front_page' => [
+ * [ 'type' => 'string', 'value' => 'Front page title and site name:'],
+ * [ 'type' => 'token', 'value' => 'site_name']
+ * ],
+ * 'posts' => [
+ * [ 'type' => 'token', 'value' => 'site_name' ],
+ * [ 'type' => 'string', 'value' => ' | ' ],
+ * [ 'type' => 'token', 'value' => 'post_title' ]
+ * ],
+ * 'pages' => [],
+ * 'groups' => [],
+ * 'archives' => []
+ * ]
+ * Custom title for given page type is created by concatenating all of the array 'value' parts.
+ * Tokens are replaced with their corresponding values for current site.
+ * Empty array signals that we are not overriding the default title for particular page type.
+ */
+
+/**
+ * Class containing utility static methods for managing SEO custom title formats.
+ */
+class Jetpack_SEO_Titles {
+ /**
+ * Site option name used to store custom title formats.
+ */
+ const TITLE_FORMATS_OPTION = 'advanced_seo_title_formats';
+
+ /**
+ * Retrieves custom title formats from site option.
+ *
+ * @return array Array of custom title formats, or empty array.
+ */
+ public static function get_custom_title_formats() {
+ if( Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
+ return get_option( self::TITLE_FORMATS_OPTION, array() );
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns tokens that are currently supported for each page type.
+ *
+ * @return array Array of allowed token strings.
+ */
+ public static function get_allowed_tokens() {
+ return array(
+ 'front_page' => array( 'site_name', 'tagline' ),
+ 'posts' => array( 'site_name', 'tagline', 'post_title' ),
+ 'pages' => array( 'site_name', 'tagline', 'page_title' ),
+ 'groups' => array( 'site_name', 'tagline', 'group_title' ),
+ 'archives' => array( 'site_name', 'tagline', 'date' ),
+ );
+ }
+
+ /**
+ * Used to modify the default title with custom SEO title.
+ *
+ * @param string $default_title Default title for current page.
+ *
+ * @return string Custom title with replaced tokens or default title.
+ */
+ public static function get_custom_title( $default_title = '' ) {
+ // Don't filter title for unsupported themes.
+ if ( self::is_conflicted_theme() ) {
+ return $default_title;
+ }
+
+ $page_type = self::get_page_type();
+
+ // Keep default title if invalid page type is supplied.
+ if ( empty( $page_type ) ) {
+ return $default_title;
+ }
+
+ $title_formats = self::get_custom_title_formats();
+
+ // Keep default title if user has not defined custom title for this page type.
+ if ( empty( $title_formats[ $page_type ] ) ) {
+ return $default_title;
+ }
+
+ if ( ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
+ return $default_title;
+ }
+
+ $custom_title = '';
+ $format_array = $title_formats[ $page_type ];
+
+ foreach ( $format_array as $item ) {
+ if ( 'token' == $item['type'] ) {
+ $custom_title .= self::get_token_value( $item['value'] );
+ } else {
+ $custom_title .= $item['value'];
+ }
+ }
+
+ return esc_html( $custom_title );
+ }
+
+ /**
+ * Returns string value for given token.
+ *
+ * @param string $token_name The token name value that should be replaced.
+ *
+ * @return string Token replacement for current site, or empty string for unknown token name.
+ */
+ public static function get_token_value( $token_name ) {
+
+ switch ( $token_name ) {
+ case 'site_name':
+ return get_bloginfo( 'name' );
+
+ case 'tagline':
+ return get_bloginfo( 'description' );
+
+ case 'post_title':
+ case 'page_title':
+ return get_the_title();
+
+ case 'group_title':
+ return single_tag_title( '', false );
+
+ case 'date':
+ return self::get_date_for_title();
+
+ default:
+ return '';
+ }
+ }
+
+ /**
+ * Returns page type for current page. We need this helper in order to determine what
+ * user defined title format should be used for custom title.
+ *
+ * @return string|bool Type of current page or false if unsupported.
+ */
+ public static function get_page_type() {
+
+ if ( is_front_page() ) {
+ return 'front_page';
+ }
+
+ if ( is_category() || is_tag() ) {
+ return 'groups';
+ }
+
+ if ( is_archive() && ! is_author() ) {
+ return 'archives';
+ }
+
+ if ( is_page() ) {
+ return 'pages';
+ }
+
+ if ( is_singular() ) {
+ return 'posts';
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the value that should be used as a replacement for the date token,
+ * depending on the archive path specified.
+ *
+ * @return string Token replacement for a given date, or empty string if no date is specified.
+ */
+ public static function get_date_for_title() {
+ // If archive year, month, and day are specified.
+ if ( is_day() ) {
+ return get_the_date();
+ }
+
+ // If archive year, and month are specified.
+ if ( is_month() ) {
+ return trim( single_month_title( ' ', false ) );
+ }
+
+ // Only archive year is specified.
+ if ( is_year() ) {
+ return get_query_var( 'year' );
+ }
+
+ return '';
+ }
+
+ /**
+ * Checks if current theme is defining custom title that won't work nicely
+ * with our custom SEO title override.
+ *
+ * @return bool True if current theme sets custom title, false otherwise.
+ */
+ public static function is_conflicted_theme() {
+ /**
+ * Can be used to specify a list of themes that use their own custom title format.
+ *
+ * If current site is using one of the themes listed as conflicting,
+ * Jetpack SEO custom title formats will be disabled.
+ *
+ * @module seo-tools
+ *
+ * @since 4.4.0
+ *
+ * @param array List of conflicted theme names. Defaults to empty array.
+ */
+ $conflicted_themes = apply_filters( 'jetpack_seo_custom_title_conflicted_themes', array() );
+
+ return isset( $conflicted_themes[ get_option( 'template' ) ] );
+ }
+
+ /**
+ * Checks if a given format conforms to predefined SEO title templates.
+ *
+ * Every format type and token must be whitelisted.
+ * @see get_allowed_tokens()
+ *
+ * @param array $title_formats Template of SEO title to check.
+ *
+ * @return bool True if the formats are valid, false otherwise.
+ */
+ public static function are_valid_title_formats( $title_formats ) {
+ $allowed_tokens = self::get_allowed_tokens();
+
+ if ( ! is_array( $title_formats ) ) {
+ return false;
+ }
+
+ foreach ( $title_formats as $format_type => $format_array ) {
+ if ( ! in_array( $format_type, array_keys( $allowed_tokens ) ) ) {
+ return false;
+ }
+
+ if ( '' === $format_array ) {
+ continue;
+ }
+
+ if ( ! is_array( $format_array ) ) {
+ return false;
+ }
+
+ foreach ( $format_array as $item ) {
+ if ( empty( $item['type'] ) || empty( $item['value'] ) ) {
+ return false;
+ }
+
+ if ( 'token' == $item['type'] ) {
+ if ( ! in_array( $item['value'], $allowed_tokens[ $format_type ] ) ) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Combines the previous values of title formats, stored as array in site options,
+ * with the new values that are provided.
+ *
+ * @param array $new_formats Array containing new title formats.
+ *
+ * @return array $result Array of updated title formats, or empty array if no update was performed.
+ */
+ public static function update_title_formats( $new_formats ) {
+ // Empty array signals that custom title shouldn't be used.
+ $empty_formats = array(
+ 'front_page' => array(),
+ 'posts' => array(),
+ 'pages' => array(),
+ 'groups' => array(),
+ 'archives' => array(),
+ );
+
+ $previous_formats = self::get_custom_title_formats();
+
+ $result = array_merge( $empty_formats, $previous_formats, $new_formats );
+
+ if ( update_option( self::TITLE_FORMATS_OPTION, $result ) ) {
+ return $result;
+ }
+
+ return array();
+ }
+}
diff --git a/plugins/jetpack/modules/seo-tools/jetpack-seo-utils.php b/plugins/jetpack/modules/seo-tools/jetpack-seo-utils.php
new file mode 100644
index 00000000..b7e4362a
--- /dev/null
+++ b/plugins/jetpack/modules/seo-tools/jetpack-seo-utils.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * Class containing utility static methods that other SEO tools are relying on.
+ */
+class Jetpack_SEO_Utils {
+ /**
+ * Site option name used to store front page meta description.
+ */
+ const FRONT_PAGE_META_OPTION = 'advanced_seo_front_page_description';
+
+ /**
+ * Old version of option name that was previously used under Free plan.
+ */
+ const GRANDFATHERED_META_OPTION = 'seo_meta_description';
+
+ /**
+ * Used to check whether SEO tools are enabled for given site.
+ *
+ * @param int $site_id Optional. Defaults to current blog id if not given.
+ *
+ * @return bool True if SEO tools are enabled, false otherwise.
+ */
+ public static function is_enabled_jetpack_seo( $site_id = 0 ) {
+ /**
+ * Can be used by SEO plugin authors to disable the conflicting output of SEO Tools.
+ *
+ * @module seo-tools
+ *
+ * @since 5.0.0
+ *
+ * @param bool True if SEO Tools should be disabled, false otherwise.
+ */
+ if ( apply_filters( 'jetpack_disable_seo_tools', false ) ) {
+ return false;
+ }
+
+ if ( function_exists( 'has_any_blog_stickers' ) ) {
+ // For WPCOM sites
+ if ( empty( $site_id ) ) {
+ $site_id = get_current_blog_id();
+ }
+
+ return has_any_blog_stickers( array( 'business-plan', 'ecommerce-plan' ), $site_id );
+ }
+
+ // For all Jetpack sites
+ return true;
+ }
+
+ /**
+ * Checks if this option was set while it was still available under free plan.
+ *
+ * @return bool True if we should enable grandfathering, false otherwise.
+ */
+ public static function has_grandfathered_front_page_meta() {
+ return ! self::is_enabled_jetpack_seo() && get_option( self::GRANDFATHERED_META_OPTION );
+ }
+
+ /**
+ * Returns front page meta description for current site.
+ *
+ * Since we allowed non-business users to set Front page meta description for some time,
+ * before bundling it with other SEO tools features that require a business plan,
+ * we are supporting grandfathering here.
+ *
+ * @return string Front page meta description string or empty string.
+ */
+ public static function get_front_page_meta_description() {
+ if ( self::is_enabled_jetpack_seo() ) {
+ $front_page_meta = get_option( self::FRONT_PAGE_META_OPTION );
+ return $front_page_meta ? $front_page_meta : get_option( self::GRANDFATHERED_META_OPTION, '' );
+ }
+
+ // Support grandfathering for non-business users.
+ return get_option( self::GRANDFATHERED_META_OPTION, '' );
+ }
+
+ /**
+ * Updates the site option value for front page meta description.
+ *
+ * We are taking care to update the correct option, in case the value is grandfathered for current site.
+ *
+ * @param $value string New value for front page meta description.
+ *
+ * @return string Saved value, or empty string if no update was performed.
+ */
+ public static function update_front_page_meta_description( $value ) {
+ $front_page_description = sanitize_text_field( $value );
+
+ /**
+ * Can be used to limit the lenght of front page meta description.
+ *
+ * @module seo-tools
+ *
+ * @since 4.4.0
+ *
+ * @param int Maximum length of front page meta description. Defaults to 300.
+ */
+ $description_max_length = apply_filters( 'jetpack_seo_front_page_description_max_length', 300 );
+
+ if ( function_exists( 'mb_substr' ) ) {
+ $front_page_description = mb_substr( $front_page_description, 0, $description_max_length );
+ } else {
+ $front_page_description = substr( $front_page_description, 0, $description_max_length );
+ }
+
+ $can_set_meta = self::is_enabled_jetpack_seo();
+ $grandfathered_meta_option = get_option( self::GRANDFATHERED_META_OPTION );
+ $has_old_meta = ! empty( $grandfathered_meta_option );
+ $option_name = self::has_grandfathered_front_page_meta() ? self::GRANDFATHERED_META_OPTION : self::FRONT_PAGE_META_OPTION;
+
+ $did_update = update_option( $option_name, $front_page_description );
+
+ if ( $did_update && $has_old_meta && $can_set_meta ) {
+ // Delete grandfathered option if user has switched to Business plan and updated meta description.
+ delete_option( 'seo_meta_description' );
+ }
+
+ if ( $did_update ) {
+ return $front_page_description;
+ }
+
+ return '';
+ }
+}
diff --git a/plugins/jetpack/modules/seo-tools/jetpack-seo.php b/plugins/jetpack/modules/seo-tools/jetpack-seo.php
new file mode 100644
index 00000000..d8bccd46
--- /dev/null
+++ b/plugins/jetpack/modules/seo-tools/jetpack-seo.php
@@ -0,0 +1,206 @@
+<?php
+
+/**
+ * An SEO expert walks into a bar, bars, pub, public house, Irish pub, drinks, beer, wine, liquor, Grey Goose, Cristal...
+ */
+class Jetpack_SEO {
+ public function __construct() {
+ add_action( 'init', array( $this, 'init' ) );
+ }
+
+ public function init() {
+ /**
+ * Can be used to prevent SEO tools from inserting custom meta tags.
+ *
+ * @module seo-tools
+ *
+ * @since 4.4.0
+ *
+ * @param bool true Should Jetpack's SEO Meta Tags be enabled. Defaults to true.
+ */
+ if ( apply_filters( 'jetpack_seo_meta_tags_enabled', true ) ) {
+ add_action( 'wp_head', array( $this, 'meta_tags' ) );
+
+ // Add support for editing page excerpts in pages, regardless of theme support.
+ add_post_type_support( 'page', 'excerpt' );
+ }
+
+ /**
+ * Can be used to prevent SEO tools form modifying site titles.
+ *
+ * @module seo-tools
+ *
+ * @since 4.4.0
+ *
+ * @param bool true Should Jetpack SEO modify site titles. Defaults to true.
+ */
+ if ( apply_filters( 'jetpack_seo_custom_titles', true ) ) {
+ // Overwrite page title with custom SEO meta title for themes that support title-tag.
+ add_filter( 'pre_get_document_title', array( 'Jetpack_SEO_Titles', 'get_custom_title' ) );
+
+ // Add overwrite support for themes that don't support title-tag.
+ add_filter( 'wp_title', array( 'Jetpack_SEO_Titles', 'get_custom_title' ) );
+ }
+
+ add_filter( 'jetpack_open_graph_tags', array( $this, 'set_custom_og_tags' ) );
+ }
+
+ private function get_authors() {
+ global $wp_query;
+
+ $authors = array();
+
+ foreach ( $wp_query->posts as $post ) {
+ $authors[] = get_the_author_meta( 'display_name', (int) $post->post_author );
+ }
+
+ $authors = array_unique( $authors );
+
+ return $authors;
+ }
+
+ public function set_custom_og_tags( $tags ) {
+ $custom_title = Jetpack_SEO_Titles::get_custom_title();
+
+ if ( ! empty( $custom_title ) ) {
+ $tags['og:title'] = $custom_title;
+ }
+
+ $post_custom_description = Jetpack_SEO_Posts::get_post_custom_description( get_post() );
+ $front_page_meta = Jetpack_SEO_Utils::get_front_page_meta_description();
+
+ if ( is_front_page() && ! empty( $front_page_meta ) ) {
+ $tags['og:description'] = $front_page_meta;
+ } else {
+ if ( ! empty( $post_custom_description ) ) {
+ $tags['og:description'] = $post_custom_description;
+ }
+ }
+
+ return $tags;
+ }
+
+ public function meta_tags() {
+ global $wp_query;
+
+ $period = '';
+ $template = '';
+ $meta = array();
+
+ /**
+ * Can be used to specify a list of themes that set their own meta tags.
+ *
+ * If current site is using one of the themes listed as conflicting, inserting Jetpack SEO
+ * meta tags will be prevented.
+ *
+ * @module seo-tools
+ *
+ * @since 4.4.0
+ *
+ * @param array List of conflicted theme names. Defaults to empty array.
+ */
+ $conflicted_themes = apply_filters( 'jetpack_seo_meta_tags_conflicted_themes', array() );
+
+ if ( isset( $conflicted_themes[ get_option( 'template' ) ] ) ) {
+ return;
+ }
+
+ $front_page_meta = Jetpack_SEO_Utils::get_front_page_meta_description();
+ $description = $front_page_meta ? $front_page_meta : get_bloginfo( 'description' );
+ $meta['description'] = trim( $description );
+
+ // Try to target things if we're on a "specific" page of any kind.
+ if ( is_singular() ) {
+ // Business users can overwrite the description.
+ if ( ! ( is_front_page() && Jetpack_SEO_Utils::get_front_page_meta_description() ) ) {
+ $description = Jetpack_SEO_Posts::get_post_description( get_post() );
+
+ if ( $description ) {
+ $description = wp_trim_words( strip_shortcodes( wp_kses( $description, array() ) ) );
+ $meta['description'] = $description;
+ }
+ }
+
+ } elseif ( is_author() ) {
+ $obj = get_queried_object();
+
+ $meta['description'] = sprintf(
+ _x( 'Read all of the posts by %1$s on %2$s', 'Read all of the posts by Author Name on Blog Title', 'jetpack' ),
+ $obj->display_name,
+ get_bloginfo( 'title' )
+ );
+ } elseif ( is_tag() || is_category() || is_tax() ) {
+ $obj = get_queried_object();
+
+ $description = get_term_field( 'description', $obj->term_id, $obj->taxonomy, 'raw' );
+
+ if ( ! is_wp_error( $description ) && '' != $description ) {
+ $meta['description'] = wp_trim_words( $description );
+ } else {
+ $authors = $this->get_authors();
+
+ $meta['description'] = wp_sprintf(
+ _x( 'Posts about %1$s written by %2$l', 'Posts about Category written by John and Bob', 'jetpack' ),
+ single_term_title( '', false ),
+ $authors
+ );
+ }
+ } elseif ( is_date() ) {
+ if ( is_year() ) {
+ $period = get_query_var( 'year' );
+
+ $template = _nx(
+ '%1$s post published by %2$l in the year %3$s', // singular
+ '%1$s posts published by %2$l in the year %3$s', // plural
+ count( $wp_query->posts ), // number
+ '10 posts published by John in the year 2012', // context
+ 'jetpack'
+ );
+ } elseif ( is_month() ) {
+ $period = date( 'F Y', mktime( 0, 0, 0, get_query_var( 'monthnum' ), 1, get_query_var( 'year' ) ) );
+
+ $template = _nx(
+ '%1$s post published by %2$l during %3$s', // singular
+ '%1$s posts published by %2$l during %3$s', // plural
+ count( $wp_query->posts ), // number
+ '10 posts publishes by John during May 2012', // context
+ 'jetpack'
+ );
+ } elseif ( is_day() ) {
+ $period = date(
+ 'F j, Y',
+ mktime( 0, 0, 0, get_query_var( 'monthnum' ), get_query_var( 'day' ), get_query_var( 'year' ) )
+ );
+
+ $template = _nx(
+ '%1$s post published by %2$l on %3$s', // singular
+ '%1$s posts published by %2$l on %3$s', // plural
+ count( $wp_query->posts ), // number
+ '10 posts published by John on May 30, 2012', // context
+ 'jetpack'
+ );
+ }
+
+ $authors = $this->get_authors();
+ $meta['description'] = wp_sprintf( $template, count( $wp_query->posts ), $authors, $period );
+ }
+
+ /**
+ * Can be used to edit the default SEO tools meta tags.
+ *
+ * @module seo-tools
+ *
+ * @since 4.4.0
+ *
+ * @param array Array that consists of meta name and meta content pairs.
+ */
+ $meta = apply_filters( 'jetpack_seo_meta_tags', $meta );
+
+ // Output them
+ foreach ( $meta as $name => $content ) {
+ if ( ! empty( $content ) ) {
+ echo '<meta name="' . esc_attr( $name ) . '" content="' . esc_attr( $content ) . '" />' . "\n";
+ }
+ }
+ }
+}
diff --git a/plugins/jetpack/modules/sharedaddy.php b/plugins/jetpack/modules/sharedaddy.php
new file mode 100644
index 00000000..6b5d83c7
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Module Name: Sharing
+ * Module Description: Add Twitter, Facebook and Google+ buttons at the bottom of each post, making it easy for visitors to share your content.
+ * Sort Order: 7
+ * Recommendation Order: 6
+ * First Introduced: 1.1
+ * Major Changes In: 1.2
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Social, Recommended
+ * Feature: Engagement
+ * Additional Search Queries: share, sharing, sharedaddy, social buttons, buttons, share facebook, share twitter, social media sharing, social media share, social share, icons, email, facebook, twitter, linkedin, pinterest, pocket, social widget, social media
+ *
+ * @package Jetpack
+ */
+
+if ( ! function_exists( 'sharing_init' ) ) {
+ require dirname( __FILE__ ) . '/sharedaddy/sharedaddy.php';
+}
+
+add_action( 'jetpack_modules_loaded', 'sharedaddy_loaded' );
+
+/**
+ * Sharing module code loaded after all modules have been loaded.
+ */
+function sharedaddy_loaded() {
+ Jetpack::enable_module_configurable( __FILE__ );
+ add_filter( 'jetpack_module_configuration_url_sharedaddy', 'jetpack_sharedaddy_configuration_url' );
+}
+
+/**
+ * Return Jetpack Sharing configuration URL
+ *
+ * @return string Sharing config URL
+ */
+function jetpack_sharedaddy_configuration_url() {
+ if ( Jetpack::is_development_mode() || Jetpack::is_staging_site() || ! Jetpack::is_user_connected() ) {
+ return admin_url( 'options-general.php?page=sharing' );
+ }
+
+ $site_suffix = Jetpack::build_raw_urls( get_home_url() );
+ return 'https://wordpress.com/marketing/sharing-buttons/' . $site_suffix;
+}
diff --git a/plugins/jetpack/modules/sharedaddy/admin-sharing-rtl.css b/plugins/jetpack/modules/sharedaddy/admin-sharing-rtl.css
new file mode 100644
index 00000000..04d10acc
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/admin-sharing-rtl.css
@@ -0,0 +1,453 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+/**
+ * Sharedaddy Admin Styles
+ */
+
+#services-config {
+ min-width: 700px;
+ width: 100%;
+ float: right;
+}
+
+#services-config h3 {
+ font-weight: normal;
+ font-size: 15px;
+ margin: 0;
+ padding: 8px 10px;
+ overflow: hidden;
+ white-space: nowrap;
+ color: #464646;
+}
+
+#available-services, #enabled-services, #live-preview {
+ padding: 0;
+ width: 100%;
+ padding-top: 20px;
+ border-spacing: 0;
+}
+
+#enabled-services .ui-sortable {
+ min-height: 50px;
+}
+
+#enabled-services {
+ padding-bottom: 20px;
+}
+
+#available-services, #enabled-services {
+ border-bottom: 2px solid #cccccc;
+}
+
+#live-preview {
+ border-bottom: 1px solid #dfdfdf;
+ padding-bottom: 60px;
+}
+
+#available-services h3, #enabled-services h3, #live-preview h3 {
+ padding: 0px;
+ margin-top: 0px;
+ margin-bottom: 1em;
+}
+
+body.settings_page_sharing .description {
+ width: 180px;
+ vertical-align: top;
+}
+
+body.settings_page_sharing .description p {
+ font-size: 13px;
+ font-style: italic;
+}
+
+body.settings_page_sharing .services {
+ padding: 0px 20px;
+ vertical-align: top;
+}
+
+body.settings_page_sharing .services ul li {
+ float: right;
+ cursor: move;
+}
+
+body.settings_page_sharing .services ul li.divider {
+ border: none;
+ padding: 0;
+ background: none;
+ cursor: default;
+}
+
+body.settings_page_sharing ul.services-hidden {
+ margin-bottom: 0;
+}
+
+/* Generic style */
+#available-services .service, #enabled-services .service {
+ margin-left: 10px;
+ padding: 5px 5px 5px 10px;
+ border-radius: 3px;
+ border: 1px solid #bbb;
+ background: #f8f8f8;
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+
+#available-services .service:hover, #enabled-services .service:hover {
+ background: #fff;
+ border: 1px solid #bbb;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+}
+
+#available-services .service.share-deprecated,
+#enabled-services .service.share-deprecated {
+ opacity: 0.5;
+ padding: 5px;
+ text-decoration: line-through;
+}
+
+#available-services .service.share-deprecated {
+ display: none;
+}
+
+/* Generic style icons */
+li.service span:before {
+ color: #555;
+ display: inline-block;
+ -webkit-font-smoothing: antialiased;
+ font: normal 18px/1 'social-logos';
+ vertical-align: top;
+ position: relative;
+ top: 1px;
+ margin-left: 3px;
+ width: 16px;
+ height: 16px;
+ text-align: center;
+}
+li.service.share-print span:before {
+ content: '\f469';
+}
+li.service.share-digg span:before {
+ content: '\f221';
+}
+li.service.share-email span:before {
+ content: '\f410';
+}
+li.service.share-linkedin span:before {
+ content: '\f207';
+}
+li.service.share-twitter span:before {
+ content: '\f202';
+}
+li.service.share-reddit span:before {
+ content: '\f222';
+}
+li.service.share-tumblr span:before {
+ content: '\f214';
+}
+li.service.share-pocket span:before {
+ content: '\f224';
+}
+li.service.share-pinterest span:before {
+ content: '\f209';
+}
+li.service.share-facebook span:before {
+ content: '\f203';
+}
+li.service.share-press-this span:before { /* Fixme: remove this button in favor of reblog */
+ content: '\f205';
+}
+li.service.share-telegram span:before {
+ content: '\f606';
+}
+li.service.share-jetpack-whatsapp span:before {
+ content: '\f608';
+}
+li.service.share-skype span:before {
+ content: '\f220';
+}
+
+/**
+ * Preview section
+ */
+
+body.settings_page_sharing ul.preview {
+ float: right;
+ margin: 0px;
+}
+
+body.settings_page_sharing ul.preview li.preview-item, body.settings_page_sharing ul.preview li.preview-item a {
+ cursor: default;
+ text-decoration: none;
+}
+
+div.sd-social-icon ul.preview li.preview-item a span,
+div.sd-social-icon .inner li.preview-item a span {
+ display: none;
+}
+
+div.sd-social-icon ul.preview li.preview-item.preview-custom a span {
+ display: inline-block;
+
+}
+
+.services .preview li.share-custom a {
+ text-decoration: none;
+}
+
+.services ul li.end-fix {
+ clear:both;
+ float:none;
+ visibility:hidden;
+ padding:0;
+ margin:0;
+ height:20px;
+ width:0;
+}
+
+#enabled-services h2{
+ font-size:20px;
+ padding-top:0px;
+ font-weight: normal !important;
+ color: #999;
+}
+
+body.settings_page_sharing #live-preview h2 {
+ font-size:20px;
+ font-weight: normal !important;
+ color: #e3e3e3;
+}
+
+body.settings_page_sharing .clearing {
+ clear: both;
+}
+
+body.settings_page_sharing .options .options-left {
+ float: right;
+}
+
+body.settings_page_sharing .input label {
+ font-size: 11px;
+ line-height: 16px;
+}
+
+body.settings_page_sharing .advanced-form {
+ padding: 10px 10px 8px 14px;
+ margin-right: -24px;
+ display: none;
+ border-top: 1px #e3e3e3 solid;
+ margin-top:4px;
+}
+
+body.settings_page_sharing .utility {
+ float: left;
+ padding-top:10px;
+ padding-left: 10px;
+ font-size: 10px;
+}
+
+body.settings_page_sharing .advanced input[type=submit] {
+ float: right;
+ margin-top:10px;
+ margin-left: 10px;
+}
+
+.services li.dropzone {
+ border: 1px dashed #999;
+ border-radius: 3px;
+ background: #e3e3e3;
+ margin-left: 10px;
+ padding: 5px;
+ height: 18px;
+}
+
+.advanced-form .form-table th {
+ width: auto !important;
+}
+
+.advanced-form .button-secondary {
+ margin-top: 0 !important;
+}
+
+#hidden-drop-target {
+ background: #e1e1e1;
+ border: 1px solid #cdcdcd;
+ width: 29%;
+ padding: 10px;
+ vertical-align: top;
+}
+
+#hidden-drop-target p {
+ font-size: 13px;
+ font-style: italic;
+ margin: 0 0 10px 0;
+}
+
+
+/* Official button previews */
+.preview li.preview-item {
+ background-position: 0px 5px;
+ cursor: default;
+}
+
+.preview .option-smart-on {
+ margin: 3px 0 0 5px;
+}
+
+.preview-digg .option-smart-on {
+ background: url(images/smart-digg.png) no-repeat top left;
+ background-size: 76px 17px;
+ width:76px;
+ height:17px;
+ margin-top: 2px;
+}
+
+.preview-reddit .option-smart-on {
+ background: url(images/smart-reddit.png) no-repeat top left;
+ background-size: 104px 21px;
+ width:104px;
+ height:21px;
+}
+
+.preview-facebook .option-smart-on {
+ background: url(images/smart-like.png) no-repeat top left;
+ background-size: 85px 20px;
+ width:85px;
+ height:20px;
+}
+
+.preview-twitter .option-smart-on {
+ background: url(images/smart-twitter.png?1) no-repeat top left;
+ background-size: 60px 20px;
+ width:60px;
+ height:20px;
+}
+
+.preview-linkedin .option-smart-on {
+ background: url(images/linkedin-smart.png) no-repeat top center;
+ background-size: 99px 18px;
+ width:99px;
+ height:20px;
+}
+
+.preview-tumblr .option-smart-on {
+ background: url(images/smart-tumblr.png) no-repeat top left;
+ background-size: 62px 20px;
+ width: 62px;
+ height: 20px;
+}
+
+.preview-pinterest .option-smart-on {
+ background: url(images/smart-pinterest.png) no-repeat top left;
+ background-size: 39px 20px;
+ width: 39px;
+ height: 20px;
+}
+
+.preview-pocket .option-smart-on {
+ background: url(images/smart-pocket.png) no-repeat top left;
+ background-size: 60px 20px;
+ width: 60px;
+ height: 20px;
+}
+
+.preview-skype .option-smart-on {
+ background: url(images/smart-skype.png) no-repeat top left;
+ background-size: 60px 20px;
+ width: 60px;
+ height: 20px;
+}
+
+.preview-item.share-deprecated {
+ opacity: 0.5;
+}
+
+.preview-item.share-deprecated a span {
+ text-decoration: line-through;
+}
+
+@media
+(-webkit-min-device-pixel-ratio: 1.25),
+(min-resolution: 120dpi) {
+ .preview-digg .option-smart-on {
+ background-image: url(images/smart-digg@2x.png);
+ }
+
+ .preview-reddit .option-smart-on {
+ background-image: url(images/smart-reddit@2x.png);
+ }
+
+ .preview-facebook .option-smart-on {
+ background-image: url(images/smart-like@2x.png);
+ }
+
+ .preview-twitter .option-smart-on {
+ background-image: url(images/smart-twitter@2x.png?1);
+ }
+
+ .preview-linkedin .option-smart-on {
+ background-image: url(images/linkedin-smart@2x.png);
+ }
+
+ .preview-tumblr .option-smart-on {
+ background-image: url(images/smart-tumblr@2x.png);
+ }
+
+ .preview-pinterest .option-smart-on {
+ background-image: url(images/smart-pinterest@2x.png);
+ }
+
+ .preview-pocket .option-smart-on {
+ background-image: url(images/smart-pocket@2x.png);
+ }
+
+ .preview-skype .option-smart-on {
+ background-image: url(images/smart-skype@2x.png);
+ }
+}
+
+/**
+ * Overflow sharing dialog
+ */
+
+.services .sharing-hidden li {
+ background-color: transparent;
+}
+
+.sharing-hidden li.share-end {
+ clear: both;
+ height: 0;
+ padding: 0px !important;
+ margin: 0px !important;
+ width: 0;
+ visibility: hidden;
+ float: none;
+}
+
+.preview .sharing-label {
+ font-weight: bold;
+ border: 0;
+ padding: 4px 0 0 6px;
+}
+
+#services-config a.remove {
+ background: #ddd;
+ color: #fff;
+ padding: 0px 4px 2px;
+ border-radius: 15px;
+ -moz-border-radius: 15px;
+ -webkit-border-radius: 15px;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 10px;
+}
+
+#services-config a.remove:hover {
+ background: #f00;
+}
+
+.sd-social-icon .inner a.sd-button span,
+.sd-social-icon .inner a.share-icon span {
+ display: inline-block;
+ overflow: hidden;
+ width: 0;
+ text-indent: 100%;
+}
diff --git a/plugins/jetpack/modules/sharedaddy/admin-sharing-rtl.min.css b/plugins/jetpack/modules/sharedaddy/admin-sharing-rtl.min.css
new file mode 100644
index 00000000..db5c3771
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/admin-sharing-rtl.min.css
@@ -0,0 +1 @@
+#services-config{min-width:700px;width:100%;float:right}#services-config h3{font-weight:400;font-size:15px;margin:0;padding:8px 10px;overflow:hidden;white-space:nowrap;color:#464646}#available-services,#enabled-services,#live-preview{padding:0;width:100%;padding-top:20px;border-spacing:0}#enabled-services .ui-sortable{min-height:50px}#enabled-services{padding-bottom:20px}#available-services,#enabled-services{border-bottom:2px solid #ccc}#live-preview{border-bottom:1px solid #dfdfdf;padding-bottom:60px}#available-services h3,#enabled-services h3,#live-preview h3{padding:0;margin-top:0;margin-bottom:1em}body.settings_page_sharing .description{width:180px;vertical-align:top}body.settings_page_sharing .description p{font-size:13px;font-style:italic}body.settings_page_sharing .services{padding:0 20px;vertical-align:top}body.settings_page_sharing .services ul li{float:right;cursor:move}body.settings_page_sharing .services ul li.divider{border:none;padding:0;background:0 0;cursor:default}body.settings_page_sharing ul.services-hidden{margin-bottom:0}#available-services .service,#enabled-services .service{margin-left:10px;padding:5px 5px 5px 10px;border-radius:3px;border:1px solid #bbb;background:#f8f8f8;background-repeat:no-repeat;background-position:center center}#available-services .service:hover,#enabled-services .service:hover{background:#fff;border:1px solid #bbb;box-shadow:0 1px 3px rgba(0,0,0,.2)}#available-services .service.share-deprecated,#enabled-services .service.share-deprecated{opacity:.5;padding:5px;text-decoration:line-through}#available-services .service.share-deprecated{display:none}li.service span:before{color:#555;display:inline-block;-webkit-font-smoothing:antialiased;font:normal 18px/1 social-logos;vertical-align:top;position:relative;top:1px;margin-left:3px;width:16px;height:16px;text-align:center}li.service.share-print span:before{content:'\f469'}li.service.share-digg span:before{content:'\f221'}li.service.share-email span:before{content:'\f410'}li.service.share-linkedin span:before{content:'\f207'}li.service.share-twitter span:before{content:'\f202'}li.service.share-reddit span:before{content:'\f222'}li.service.share-tumblr span:before{content:'\f214'}li.service.share-pocket span:before{content:'\f224'}li.service.share-pinterest span:before{content:'\f209'}li.service.share-facebook span:before{content:'\f203'}li.service.share-press-this span:before{content:'\f205'}li.service.share-telegram span:before{content:'\f606'}li.service.share-jetpack-whatsapp span:before{content:'\f608'}li.service.share-skype span:before{content:'\f220'}body.settings_page_sharing ul.preview{float:right;margin:0}body.settings_page_sharing ul.preview li.preview-item,body.settings_page_sharing ul.preview li.preview-item a{cursor:default;text-decoration:none}div.sd-social-icon .inner li.preview-item a span,div.sd-social-icon ul.preview li.preview-item a span{display:none}div.sd-social-icon ul.preview li.preview-item.preview-custom a span{display:inline-block}.services .preview li.share-custom a{text-decoration:none}.services ul li.end-fix{clear:both;float:none;visibility:hidden;padding:0;margin:0;height:20px;width:0}#enabled-services h2{font-size:20px;padding-top:0;font-weight:400!important;color:#999}body.settings_page_sharing #live-preview h2{font-size:20px;font-weight:400!important;color:#e3e3e3}body.settings_page_sharing .clearing{clear:both}body.settings_page_sharing .options .options-left{float:right}body.settings_page_sharing .input label{font-size:11px;line-height:16px}body.settings_page_sharing .advanced-form{padding:10px 10px 8px 14px;margin-right:-24px;display:none;border-top:1px #e3e3e3 solid;margin-top:4px}body.settings_page_sharing .utility{float:left;padding-top:10px;padding-left:10px;font-size:10px}body.settings_page_sharing .advanced input[type=submit]{float:right;margin-top:10px;margin-left:10px}.services li.dropzone{border:1px dashed #999;border-radius:3px;background:#e3e3e3;margin-left:10px;padding:5px;height:18px}.advanced-form .form-table th{width:auto!important}.advanced-form .button-secondary{margin-top:0!important}#hidden-drop-target{background:#e1e1e1;border:1px solid #cdcdcd;width:29%;padding:10px;vertical-align:top}#hidden-drop-target p{font-size:13px;font-style:italic;margin:0 0 10px 0}.preview li.preview-item{background-position:0 5px;cursor:default}.preview .option-smart-on{margin:3px 0 0 5px}.preview-digg .option-smart-on{background:url(images/smart-digg.png) no-repeat top left;background-size:76px 17px;width:76px;height:17px;margin-top:2px}.preview-reddit .option-smart-on{background:url(images/smart-reddit.png) no-repeat top left;background-size:104px 21px;width:104px;height:21px}.preview-facebook .option-smart-on{background:url(images/smart-like.png) no-repeat top left;background-size:85px 20px;width:85px;height:20px}.preview-twitter .option-smart-on{background:url(images/smart-twitter.png?1) no-repeat top left;background-size:60px 20px;width:60px;height:20px}.preview-linkedin .option-smart-on{background:url(images/linkedin-smart.png) no-repeat top center;background-size:99px 18px;width:99px;height:20px}.preview-tumblr .option-smart-on{background:url(images/smart-tumblr.png) no-repeat top left;background-size:62px 20px;width:62px;height:20px}.preview-pinterest .option-smart-on{background:url(images/smart-pinterest.png) no-repeat top left;background-size:39px 20px;width:39px;height:20px}.preview-pocket .option-smart-on{background:url(images/smart-pocket.png) no-repeat top left;background-size:60px 20px;width:60px;height:20px}.preview-skype .option-smart-on{background:url(images/smart-skype.png) no-repeat top left;background-size:60px 20px;width:60px;height:20px}.preview-item.share-deprecated{opacity:.5}.preview-item.share-deprecated a span{text-decoration:line-through}@media (-webkit-min-device-pixel-ratio:1.25),(min-resolution:120dpi){.preview-digg .option-smart-on{background-image:url(images/smart-digg@2x.png)}.preview-reddit .option-smart-on{background-image:url(images/smart-reddit@2x.png)}.preview-facebook .option-smart-on{background-image:url(images/smart-like@2x.png)}.preview-twitter .option-smart-on{background-image:url(images/smart-twitter@2x.png?1)}.preview-linkedin .option-smart-on{background-image:url(images/linkedin-smart@2x.png)}.preview-tumblr .option-smart-on{background-image:url(images/smart-tumblr@2x.png)}.preview-pinterest .option-smart-on{background-image:url(images/smart-pinterest@2x.png)}.preview-pocket .option-smart-on{background-image:url(images/smart-pocket@2x.png)}.preview-skype .option-smart-on{background-image:url(images/smart-skype@2x.png)}}.services .sharing-hidden li{background-color:transparent}.sharing-hidden li.share-end{clear:both;height:0;padding:0!important;margin:0!important;width:0;visibility:hidden;float:none}.preview .sharing-label{font-weight:700;border:0;padding:4px 0 0 6px}#services-config a.remove{background:#ddd;color:#fff;padding:0 4px 2px;border-radius:15px;-moz-border-radius:15px;-webkit-border-radius:15px;text-decoration:none;font-weight:700;font-size:10px}#services-config a.remove:hover{background:red}.sd-social-icon .inner a.sd-button span,.sd-social-icon .inner a.share-icon span{display:inline-block;overflow:hidden;width:0;text-indent:100%} \ No newline at end of file
diff --git a/plugins/jetpack/modules/sharedaddy/admin-sharing.css b/plugins/jetpack/modules/sharedaddy/admin-sharing.css
new file mode 100644
index 00000000..f7093186
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/admin-sharing.css
@@ -0,0 +1,452 @@
+/**
+ * Sharedaddy Admin Styles
+ */
+
+#services-config {
+ min-width: 700px;
+ width: 100%;
+ float: left;
+}
+
+#services-config h3 {
+ font-weight: normal;
+ font-size: 15px;
+ margin: 0;
+ padding: 8px 10px;
+ overflow: hidden;
+ white-space: nowrap;
+ color: #464646;
+}
+
+#available-services, #enabled-services, #live-preview {
+ padding: 0;
+ width: 100%;
+ padding-top: 20px;
+ border-spacing: 0;
+}
+
+#enabled-services .ui-sortable {
+ min-height: 50px;
+}
+
+#enabled-services {
+ padding-bottom: 20px;
+}
+
+#available-services, #enabled-services {
+ border-bottom: 2px solid #cccccc;
+}
+
+#live-preview {
+ border-bottom: 1px solid #dfdfdf;
+ padding-bottom: 60px;
+}
+
+#available-services h3, #enabled-services h3, #live-preview h3 {
+ padding: 0px;
+ margin-top: 0px;
+ margin-bottom: 1em;
+}
+
+body.settings_page_sharing .description {
+ width: 180px;
+ vertical-align: top;
+}
+
+body.settings_page_sharing .description p {
+ font-size: 13px;
+ font-style: italic;
+}
+
+body.settings_page_sharing .services {
+ padding: 0px 20px;
+ vertical-align: top;
+}
+
+body.settings_page_sharing .services ul li {
+ float: left;
+ cursor: move;
+}
+
+body.settings_page_sharing .services ul li.divider {
+ border: none;
+ padding: 0;
+ background: none;
+ cursor: default;
+}
+
+body.settings_page_sharing ul.services-hidden {
+ margin-bottom: 0;
+}
+
+/* Generic style */
+#available-services .service, #enabled-services .service {
+ margin-right: 10px;
+ padding: 5px 10px 5px 5px;
+ border-radius: 3px;
+ border: 1px solid #bbb;
+ background: #f8f8f8;
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+
+#available-services .service:hover, #enabled-services .service:hover {
+ background: #fff;
+ border: 1px solid #bbb;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+}
+
+#available-services .service.share-deprecated,
+#enabled-services .service.share-deprecated {
+ opacity: 0.5;
+ padding: 5px;
+ text-decoration: line-through;
+}
+
+#available-services .service.share-deprecated {
+ display: none;
+}
+
+/* Generic style icons */
+li.service span:before {
+ color: #555;
+ display: inline-block;
+ -webkit-font-smoothing: antialiased;
+ font: normal 18px/1 'social-logos';
+ vertical-align: top;
+ position: relative;
+ top: 1px;
+ margin-right: 3px;
+ width: 16px;
+ height: 16px;
+ text-align: center;
+}
+li.service.share-print span:before {
+ content: '\f469';
+}
+li.service.share-digg span:before {
+ content: '\f221';
+}
+li.service.share-email span:before {
+ content: '\f410';
+}
+li.service.share-linkedin span:before {
+ content: '\f207';
+}
+li.service.share-twitter span:before {
+ content: '\f202';
+}
+li.service.share-reddit span:before {
+ content: '\f222';
+}
+li.service.share-tumblr span:before {
+ content: '\f214';
+}
+li.service.share-pocket span:before {
+ content: '\f224';
+}
+li.service.share-pinterest span:before {
+ content: '\f209';
+}
+li.service.share-facebook span:before {
+ content: '\f203';
+}
+li.service.share-press-this span:before { /* Fixme: remove this button in favor of reblog */
+ content: '\f205';
+}
+li.service.share-telegram span:before {
+ content: '\f606';
+}
+li.service.share-jetpack-whatsapp span:before {
+ content: '\f608';
+}
+li.service.share-skype span:before {
+ content: '\f220';
+}
+
+/**
+ * Preview section
+ */
+
+body.settings_page_sharing ul.preview {
+ float: left;
+ margin: 0px;
+}
+
+body.settings_page_sharing ul.preview li.preview-item, body.settings_page_sharing ul.preview li.preview-item a {
+ cursor: default;
+ text-decoration: none;
+}
+
+div.sd-social-icon ul.preview li.preview-item a span,
+div.sd-social-icon .inner li.preview-item a span {
+ display: none;
+}
+
+div.sd-social-icon ul.preview li.preview-item.preview-custom a span {
+ display: inline-block;
+
+}
+
+.services .preview li.share-custom a {
+ text-decoration: none;
+}
+
+.services ul li.end-fix {
+ clear:both;
+ float:none;
+ visibility:hidden;
+ padding:0;
+ margin:0;
+ height:20px;
+ width:0;
+}
+
+#enabled-services h2{
+ font-size:20px;
+ padding-top:0px;
+ font-weight: normal !important;
+ color: #999;
+}
+
+body.settings_page_sharing #live-preview h2 {
+ font-size:20px;
+ font-weight: normal !important;
+ color: #e3e3e3;
+}
+
+body.settings_page_sharing .clearing {
+ clear: both;
+}
+
+body.settings_page_sharing .options .options-left {
+ float: left;
+}
+
+body.settings_page_sharing .input label {
+ font-size: 11px;
+ line-height: 16px;
+}
+
+body.settings_page_sharing .advanced-form {
+ padding: 10px 14px 8px 10px;
+ margin-left: -24px;
+ display: none;
+ border-top: 1px #e3e3e3 solid;
+ margin-top:4px;
+}
+
+body.settings_page_sharing .utility {
+ float: right;
+ padding-top:10px;
+ padding-right: 10px;
+ font-size: 10px;
+}
+
+body.settings_page_sharing .advanced input[type=submit] {
+ float: left;
+ margin-top:10px;
+ margin-right: 10px;
+}
+
+.services li.dropzone {
+ border: 1px dashed #999;
+ border-radius: 3px;
+ background: #e3e3e3;
+ margin-right: 10px;
+ padding: 5px;
+ height: 18px;
+}
+
+.advanced-form .form-table th {
+ width: auto !important;
+}
+
+.advanced-form .button-secondary {
+ margin-top: 0 !important;
+}
+
+#hidden-drop-target {
+ background: #e1e1e1;
+ border: 1px solid #cdcdcd;
+ width: 29%;
+ padding: 10px;
+ vertical-align: top;
+}
+
+#hidden-drop-target p {
+ font-size: 13px;
+ font-style: italic;
+ margin: 0 0 10px 0;
+}
+
+
+/* Official button previews */
+.preview li.preview-item {
+ background-position: 0px 5px;
+ cursor: default;
+}
+
+.preview .option-smart-on {
+ margin: 3px 5px 0 0;
+}
+
+.preview-digg .option-smart-on {
+ background: url(images/smart-digg.png) no-repeat top left;
+ background-size: 76px 17px;
+ width:76px;
+ height:17px;
+ margin-top: 2px;
+}
+
+.preview-reddit .option-smart-on {
+ background: url(images/smart-reddit.png) no-repeat top left;
+ background-size: 104px 21px;
+ width:104px;
+ height:21px;
+}
+
+.preview-facebook .option-smart-on {
+ background: url(images/smart-like.png) no-repeat top left;
+ background-size: 85px 20px;
+ width:85px;
+ height:20px;
+}
+
+.preview-twitter .option-smart-on {
+ background: url(images/smart-twitter.png?1) no-repeat top left;
+ background-size: 60px 20px;
+ width:60px;
+ height:20px;
+}
+
+.preview-linkedin .option-smart-on {
+ background: url(images/linkedin-smart.png) no-repeat top center;
+ background-size: 99px 18px;
+ width:99px;
+ height:20px;
+}
+
+.preview-tumblr .option-smart-on {
+ background: url(images/smart-tumblr.png) no-repeat top left;
+ background-size: 62px 20px;
+ width: 62px;
+ height: 20px;
+}
+
+.preview-pinterest .option-smart-on {
+ background: url(images/smart-pinterest.png) no-repeat top left;
+ background-size: 39px 20px;
+ width: 39px;
+ height: 20px;
+}
+
+.preview-pocket .option-smart-on {
+ background: url(images/smart-pocket.png) no-repeat top left;
+ background-size: 60px 20px;
+ width: 60px;
+ height: 20px;
+}
+
+.preview-skype .option-smart-on {
+ background: url(images/smart-skype.png) no-repeat top left;
+ background-size: 60px 20px;
+ width: 60px;
+ height: 20px;
+}
+
+.preview-item.share-deprecated {
+ opacity: 0.5;
+}
+
+.preview-item.share-deprecated a span {
+ text-decoration: line-through;
+}
+
+@media
+(-webkit-min-device-pixel-ratio: 1.25),
+(min-resolution: 120dpi) {
+ .preview-digg .option-smart-on {
+ background-image: url(images/smart-digg@2x.png);
+ }
+
+ .preview-reddit .option-smart-on {
+ background-image: url(images/smart-reddit@2x.png);
+ }
+
+ .preview-facebook .option-smart-on {
+ background-image: url(images/smart-like@2x.png);
+ }
+
+ .preview-twitter .option-smart-on {
+ background-image: url(images/smart-twitter@2x.png?1);
+ }
+
+ .preview-linkedin .option-smart-on {
+ background-image: url(images/linkedin-smart@2x.png);
+ }
+
+ .preview-tumblr .option-smart-on {
+ background-image: url(images/smart-tumblr@2x.png);
+ }
+
+ .preview-pinterest .option-smart-on {
+ background-image: url(images/smart-pinterest@2x.png);
+ }
+
+ .preview-pocket .option-smart-on {
+ background-image: url(images/smart-pocket@2x.png);
+ }
+
+ .preview-skype .option-smart-on {
+ background-image: url(images/smart-skype@2x.png);
+ }
+}
+
+/**
+ * Overflow sharing dialog
+ */
+
+.services .sharing-hidden li {
+ background-color: transparent;
+}
+
+.sharing-hidden li.share-end {
+ clear: both;
+ height: 0;
+ padding: 0px !important;
+ margin: 0px !important;
+ width: 0;
+ visibility: hidden;
+ float: none;
+}
+
+.preview .sharing-label {
+ font-weight: bold;
+ border: 0;
+ padding: 4px 6px 0 0;
+}
+
+#services-config a.remove {
+ background: #ddd;
+ color: #fff;
+ padding: 0px 4px 2px;
+ border-radius: 15px;
+ -moz-border-radius: 15px;
+ -webkit-border-radius: 15px;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 10px;
+}
+
+#services-config a.remove:hover {
+ background: #f00;
+}
+
+.sd-social-icon .inner a.sd-button span,
+.sd-social-icon .inner a.share-icon span {
+ display: inline-block;
+ overflow: hidden;
+ width: 0;
+ text-indent: 100%;
+}
diff --git a/plugins/jetpack/modules/sharedaddy/admin-sharing.js b/plugins/jetpack/modules/sharedaddy/admin-sharing.js
new file mode 100644
index 00000000..741931d1
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/admin-sharing.js
@@ -0,0 +1,532 @@
+/* jshint onevar: false, smarttabs: true */
+/* global sharing_loading_icon */
+
+( function( $ ) {
+ $( document ).ready( function() {
+ function enable_share_button() {
+ $( '.preview a.sharing-anchor' )
+ .unbind( 'mouseenter mouseenter' )
+ .hover(
+ function() {
+ if ( $( this ).data( 'hasappeared' ) !== true ) {
+ var item = $( '.sharing-hidden .inner' );
+ var original = $( this ).parents( 'li' );
+
+ // Create a timer to make the area appear if the mouse hovers for a period
+ var timer = setTimeout( function() {
+ $( item )
+ .css( {
+ left: $( original ).position().left + 'px',
+ top: $( original ).position().top + $( original ).height() + 3 + 'px',
+ } )
+ .slideDown( 200, function() {
+ // Mark the item as have being appeared by the hover
+ $( original )
+ .data( 'hasappeared', true )
+ .data( 'hasoriginal', true )
+ .data( 'hasitem', false );
+
+ // Remove all special handlers
+ $( item )
+ .mouseleave( handler_item_leave )
+ .mouseenter( handler_item_enter );
+ $( original )
+ .mouseleave( handler_original_leave )
+ .mouseenter( handler_original_enter );
+
+ // Add a special handler to quickly close the item
+ $( original ).click( close_it );
+ } );
+
+ // The following handlers take care of the mouseenter/mouseleave for the share button and the share area - if both are left then we close the share area
+ var handler_item_leave = function() {
+ $( original ).data( 'hasitem', false );
+
+ if ( $( original ).data( 'hasoriginal' ) === false ) {
+ var timer = setTimeout( close_it, 800 );
+ $( original ).data( 'timer2', timer );
+ }
+ };
+
+ var handler_item_enter = function() {
+ $( original ).data( 'hasitem', true );
+ clearTimeout( $( original ).data( 'timer2' ) );
+ };
+
+ var handler_original_leave = function() {
+ $( original ).data( 'hasoriginal', false );
+
+ if ( $( original ).data( 'hasitem' ) === false ) {
+ var timer = setTimeout( close_it, 800 );
+ $( original ).data( 'timer2', timer );
+ }
+ };
+
+ var handler_original_enter = function() {
+ $( original ).data( 'hasoriginal', true );
+ clearTimeout( $( original ).data( 'timer2' ) );
+ };
+
+ var close_it = function() {
+ item.slideUp( 200 );
+
+ // Clear all hooks
+ $( original )
+ .unbind( 'mouseleave', handler_original_leave )
+ .unbind( 'mouseenter', handler_original_enter );
+ $( item )
+ .unbind( 'mouseleave', handler_item_leave )
+ .unbind( 'mouseenter', handler_item_leave );
+ $( original ).data( 'hasappeared', false );
+ $( original ).unbind( 'click', close_it );
+ return false;
+ };
+ }, 200 );
+
+ // Remember the timer so we can detect it on the mouseout
+ $( this ).data( 'timer', timer );
+ }
+ },
+ function() {
+ // Mouse out - remove any timer
+ clearTimeout( $( this ).data( 'timer' ) );
+ $( this ).data( 'timer', false );
+ }
+ );
+ }
+
+ function update_preview() {
+ var button_style = $( '#button_style' ).val();
+
+ // Clear the live preview
+ $( '#live-preview ul.preview li' ).remove();
+
+ // Add label
+ if (
+ $( '#save-enabled-shares input[name=visible]' ).val() ||
+ $( '#save-enabled-shares input[name=hidden]' ).val()
+ ) {
+ $( '#live-preview ul.preview' ).append(
+ $( '#live-preview ul.archive .sharing-label' ).clone()
+ );
+ }
+
+ // Re-insert all the enabled items
+ $( 'ul.services-enabled li' ).each( function() {
+ if ( $( this ).hasClass( 'service' ) ) {
+ var service = $( this ).attr( 'id' );
+ $( '#live-preview ul.preview' ).append(
+ $( '#live-preview ul.archive li.preview-' + service ).clone()
+ );
+ }
+ } );
+
+ // Add any hidden items
+ if ( $( '#save-enabled-shares input[name=hidden]' ).val() ) {
+ // Add share button
+ $( '#live-preview ul.preview' ).append(
+ $( '#live-preview ul.archive .share-more' )
+ .parent()
+ .clone()
+ );
+
+ $( '.sharing-hidden ul li' ).remove();
+
+ // Add hidden items into the inner panel
+ $( 'ul.services-hidden li' ).each( function(/*pos, item*/) {
+ if ( $( this ).hasClass( 'service' ) ) {
+ var service = $( this ).attr( 'id' );
+ $( '.sharing-hidden .inner ul' ).append(
+ $( '#live-preview ul.archive .preview-' + service ).clone()
+ );
+ }
+ } );
+
+ enable_share_button();
+ }
+
+ $( '#live-preview div.sharedaddy' ).removeClass( 'sd-social-icon' );
+ $( '#live-preview li.advanced' ).removeClass( 'no-icon' );
+
+ // Button style
+ if ( 'icon' === button_style ) {
+ $( '#live-preview ul.preview div span, .sharing-hidden .inner ul div span' )
+ .html( '&nbsp;' )
+ .parent()
+ .addClass( 'no-text' );
+ $( '#live-preview div.sharedaddy' ).addClass( 'sd-social-icon' );
+ } else if ( 'official' === button_style ) {
+ $( '#live-preview ul.preview .advanced, .sharing-hidden .inner ul .advanced' ).each(
+ function(/*i*/) {
+ if (
+ ! $( this ).hasClass( 'preview-press-this' ) &&
+ ! $( this ).hasClass( 'preview-email' ) &&
+ ! $( this ).hasClass( 'preview-print' ) &&
+ ! $( this ).hasClass( 'preview-telegram' ) &&
+ ! $( this ).hasClass( 'preview-jetpack-whatsapp' ) &&
+ ! $( this ).hasClass( 'share-custom' ) &&
+ ! $( this ).hasClass( 'share-deprecated' )
+ ) {
+ $( this )
+ .find( '.option a span' )
+ .html( '' )
+ .parent()
+ .removeClass( 'sd-button' )
+ .parent()
+ .attr( 'class', 'option option-smart-on' );
+ }
+ }
+ );
+ } else if ( 'text' === button_style ) {
+ $( '#live-preview li.advanced' ).addClass( 'no-icon' );
+ }
+ }
+
+ window.sharing_option_changed = function() {
+ var item = this;
+
+ // Loading icon
+ $( this )
+ .parents( 'li:first' )
+ .css( 'backgroundImage', 'url("' + sharing_loading_icon + '")' );
+
+ // Save
+ $( this )
+ .parents( 'form' )
+ .ajaxSubmit( function( response ) {
+ if ( response.indexOf( '<!---' ) >= 0 ) {
+ var button = response.substring( 0, response.indexOf( '<!--->' ) );
+ var preview = response.substring( response.indexOf( '<!--->' ) + 6 );
+
+ if ( $( item ).is( ':submit' ) === true ) {
+ // Update the DOM using a bit of cut/paste technology
+
+ $( item )
+ .parents( 'li:first' )
+ .replaceWith( button );
+ }
+
+ $(
+ '#live-preview ul.archive li.preview-' +
+ $( item )
+ .parents( 'form' )
+ .find( 'input[name=service]' )
+ .val()
+ ).replaceWith( preview );
+ }
+
+ // Update preview
+ update_preview();
+
+ // Restore the icon
+ $( item )
+ .parents( 'li:first' )
+ .removeAttr( 'style' );
+ } );
+
+ if ( $( item ).is( ':submit' ) === true ) {
+ return false;
+ }
+ return true;
+ };
+
+ function showExtraOptions( service ) {
+ jQuery( '.' + service + '-extra-options' )
+ .css( { backgroundColor: '#ffffcc' } )
+ .fadeIn();
+ }
+
+ function hideExtraOptions( service ) {
+ jQuery( '.' + service + '-extra-options' ).fadeOut( 'slow' );
+ }
+
+ function save_services() {
+ $( '#enabled-services h3 img' ).show();
+
+ // Toggle various dividers/help texts
+ if ( $( '#enabled-services ul.services-enabled li.service' ).length > 0 ) {
+ $( '#drag-instructions' ).hide();
+ } else {
+ $( '#drag-instructions' ).show();
+ }
+
+ if ( $( '#enabled-services li.service' ).length > 0 ) {
+ $( '#live-preview .services h2' ).hide();
+ } else {
+ $( '#live-preview .services h2' ).show();
+ }
+
+ // Gather the modules
+ var visible = [],
+ hidden = [];
+
+ $( 'ul.services-enabled li' ).each( function() {
+ if ( $( this ).hasClass( 'service' ) ) {
+ // Ready for saving
+ visible[ visible.length ] = $( this ).attr( 'id' );
+ showExtraOptions( $( this ).attr( 'id' ) );
+ }
+ } );
+
+ $( 'ul.services-available li' ).each( function() {
+ if ( $( this ).hasClass( 'service' ) ) {
+ hideExtraOptions( $( this ).attr( 'id' ) );
+ }
+ } );
+
+ $( 'ul.services-hidden li' ).each( function() {
+ if ( $( this ).hasClass( 'service' ) ) {
+ // Ready for saving
+ hidden[ hidden.length ] = $( this ).attr( 'id' );
+ showExtraOptions( $( this ).attr( 'id' ) );
+ }
+ } );
+
+ // Set the hidden element values
+ $( '#save-enabled-shares input[name=visible]' ).val( visible.join( ',' ) );
+ $( '#save-enabled-shares input[name=hidden]' ).val( hidden.join( ',' ) );
+
+ update_preview();
+
+ // Save it
+ $( '#save-enabled-shares' ).ajaxSubmit( function() {
+ $( '#enabled-services h3 img' ).hide();
+ } );
+ }
+
+ $( '#enabled-services .services ul' ).sortable( {
+ receive: function(/*event, ui*/) {
+ save_services();
+ },
+ stop: function() {
+ save_services();
+ $( 'li.service' ).enableSelection(); // Fixes a problem with Chrome
+ },
+ over: function(/*event, ui*/) {
+ $( this )
+ .find( 'ul' )
+ .addClass( 'dropping' );
+
+ // Ensure the 'end-fix' is at the end
+ $( '#enabled-services li.end-fix' ).remove();
+ $( '#enabled-services ul' ).append( '<li class="end-fix"></li>' );
+ },
+ out: function(/*event, ui*/) {
+ $( this )
+ .find( 'ul' )
+ .removeClass( 'dropping' );
+
+ // Ensure the 'end-fix' is at the end
+ $( '#enabled-services li.end-fix' ).remove();
+ $( '#enabled-services ul' ).append( '<li class="end-fix"></li>' );
+ },
+ helper: function( event, ui ) {
+ ui.find( '.advanced-form' ).hide();
+
+ return ui.clone();
+ },
+ start: function(/*event, ui*/) {
+ // Make sure that the advanced section is closed
+ $( '.advanced-form' ).hide();
+ $( 'li.service' ).disableSelection(); // Fixes a problem with Chrome
+ },
+ placeholder: 'dropzone',
+ opacity: 0.8,
+ delay: 150,
+ forcePlaceholderSize: true,
+ items: 'li',
+ connectWith: '#available-services ul, #enabled-services .services ul',
+ cancel: '.advanced-form',
+ } );
+
+ $( '#available-services ul' ).sortable( {
+ opacity: 0.8,
+ delay: 150,
+ cursor: 'move',
+ connectWith: '#enabled-services .services ul',
+ placeholder: 'dropzone',
+ forcePlaceholderSize: true,
+ start: function() {
+ $( '.advanced-form' ).hide();
+ },
+ } );
+
+ // Accessibility keyboard shortcurts
+ $( '.service' ).on( 'keydown', function( e ) {
+ // Reposition if one of the directional keys is pressed
+ switch ( e.keyCode ) {
+ case 13:
+ keyboardDragDrop( $( this ) );
+ break; // Enter
+ case 32:
+ keyboardDragDrop( $( this ) );
+ break; // Space
+ case 37:
+ keyboardChangeOrder( $( this ), 'left' );
+ break; // Left
+ case 39:
+ keyboardChangeOrder( $( this ), 'right' );
+ break; // Right
+ default:
+ return true; // Exit and bubble
+ }
+
+ e.preventDefault();
+ } );
+
+ function keyboardChangeOrder( $this, dir ) {
+ var thisParent = $this.parent(),
+ thisParentsChildren = thisParent.find( 'li' ),
+ thisPosition = thisParentsChildren.index( $this ) + 1,
+ totalChildren = thisParentsChildren.length - 1,
+ thisService;
+
+ // No need to be able to sort order for the "Available Services" section
+ if ( thisParent.hasClass( 'services-available' ) ) {
+ return;
+ }
+
+ if ( 'left' === dir ) {
+ if ( 1 === thisPosition ) {
+ return;
+ }
+
+ // Find service to left
+ var prevSibling = $this.prev();
+
+ // Detach this service from DOM
+ thisService = $this.detach();
+
+ // Move it to the appropriate area and add focus back to service
+ prevSibling.before( thisService );
+
+ // Add focus
+ prevSibling.prev().focus();
+ }
+
+ if ( 'right' === dir ) {
+ if ( thisPosition === totalChildren ) {
+ return;
+ }
+
+ // Find service to left
+ var nextSibling = $this.next();
+
+ // Detach this service from DOM
+ thisService = $this.detach();
+
+ // Move it to the appropriate area and add focus back to service
+ nextSibling.after( thisService );
+
+ // Add focus
+ nextSibling.next().focus();
+ }
+
+ //Save changes
+ save_services();
+ }
+
+ function keyboardDragDrop( $this ) {
+ var dropzone,
+ thisParent = $this.parent();
+
+ // Rotate through 3 available dropzones
+ if ( thisParent.hasClass( 'services-available' ) ) {
+ dropzone = 'services-enabled';
+ } else if ( thisParent.hasClass( 'services-enabled' ) ) {
+ dropzone = 'services-hidden';
+ } else {
+ dropzone = 'services-available';
+ }
+
+ // Detach this service from DOM
+ var thisService = $this.detach();
+
+ // Move it to the appropriate area and add focus back to service
+ $( '.' + dropzone )
+ .prepend( thisService )
+ .find( 'li:first-child' )
+ .focus();
+
+ //Save changes
+ save_services();
+ }
+
+ // Live preview 'hidden' button
+ $( '.preview-hidden a' ).click( function() {
+ $( this )
+ .parent()
+ .find( '.preview' )
+ .toggle();
+ return false;
+ } );
+
+ // Add service
+ $( '#new-service form' ).ajaxForm( {
+ beforeSubmit: function() {
+ $( '#new-service-form .error' ).hide();
+ $( '#new-service-form img' ).show();
+ $( '#new-service-form input[type=submit]' ).prop( 'disabled', true );
+ },
+ success: function( response ) {
+ $( '#new-service-form img' ).hide();
+
+ if ( '' + response === '1' ) {
+ $( '#new-service-form .inerror' )
+ .removeClass( 'inerror' )
+ .addClass( 'error' );
+ $( '#new-service-form .error' ).show();
+ $( '#new-service-form input[type=submit]' ).prop( 'disabled', false );
+ } else {
+ document.location.reload();
+ }
+ },
+ } );
+
+ function init_handlers() {
+ $( '#services-config a.remove' )
+ .unbind( 'click' )
+ .click( function() {
+ var form = $( this )
+ .parent()
+ .next();
+
+ // Loading icon
+ $( this )
+ .parents( 'li:first' )
+ .css( 'backgroundImage', 'url("' + sharing_loading_icon + '")' );
+
+ // Save
+ form.ajaxSubmit( function(/*response*/) {
+ // Remove the item
+ form.parents( 'li:first' ).fadeOut( function() {
+ $( this ).remove();
+
+ // Update preview
+ update_preview();
+ } );
+ } );
+
+ return false;
+ } );
+ }
+
+ $( '#button_style' )
+ .change( function() {
+ update_preview();
+ return true;
+ } )
+ .change();
+
+ $( 'input[name=sharing_label]' ).blur( function() {
+ $( '#live-preview h3.sd-title' ).text(
+ $( '<div/>' )
+ .text( $( this ).val() )
+ .html()
+ );
+ } );
+
+ init_handlers();
+ enable_share_button();
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/sharedaddy/admin-sharing.min.css b/plugins/jetpack/modules/sharedaddy/admin-sharing.min.css
new file mode 100644
index 00000000..e572752d
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/admin-sharing.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#services-config{min-width:700px;width:100%;float:left}#services-config h3{font-weight:400;font-size:15px;margin:0;padding:8px 10px;overflow:hidden;white-space:nowrap;color:#464646}#available-services,#enabled-services,#live-preview{padding:0;width:100%;padding-top:20px;border-spacing:0}#enabled-services .ui-sortable{min-height:50px}#enabled-services{padding-bottom:20px}#available-services,#enabled-services{border-bottom:2px solid #ccc}#live-preview{border-bottom:1px solid #dfdfdf;padding-bottom:60px}#available-services h3,#enabled-services h3,#live-preview h3{padding:0;margin-top:0;margin-bottom:1em}body.settings_page_sharing .description{width:180px;vertical-align:top}body.settings_page_sharing .description p{font-size:13px;font-style:italic}body.settings_page_sharing .services{padding:0 20px;vertical-align:top}body.settings_page_sharing .services ul li{float:left;cursor:move}body.settings_page_sharing .services ul li.divider{border:none;padding:0;background:0 0;cursor:default}body.settings_page_sharing ul.services-hidden{margin-bottom:0}#available-services .service,#enabled-services .service{margin-right:10px;padding:5px 10px 5px 5px;border-radius:3px;border:1px solid #bbb;background:#f8f8f8;background-repeat:no-repeat;background-position:center center}#available-services .service:hover,#enabled-services .service:hover{background:#fff;border:1px solid #bbb;box-shadow:0 1px 3px rgba(0,0,0,.2)}#available-services .service.share-deprecated,#enabled-services .service.share-deprecated{opacity:.5;padding:5px;text-decoration:line-through}#available-services .service.share-deprecated{display:none}li.service span:before{color:#555;display:inline-block;-webkit-font-smoothing:antialiased;font:normal 18px/1 social-logos;vertical-align:top;position:relative;top:1px;margin-right:3px;width:16px;height:16px;text-align:center}li.service.share-print span:before{content:'\f469'}li.service.share-digg span:before{content:'\f221'}li.service.share-email span:before{content:'\f410'}li.service.share-linkedin span:before{content:'\f207'}li.service.share-twitter span:before{content:'\f202'}li.service.share-reddit span:before{content:'\f222'}li.service.share-tumblr span:before{content:'\f214'}li.service.share-pocket span:before{content:'\f224'}li.service.share-pinterest span:before{content:'\f209'}li.service.share-facebook span:before{content:'\f203'}li.service.share-press-this span:before{content:'\f205'}li.service.share-telegram span:before{content:'\f606'}li.service.share-jetpack-whatsapp span:before{content:'\f608'}li.service.share-skype span:before{content:'\f220'}body.settings_page_sharing ul.preview{float:left;margin:0}body.settings_page_sharing ul.preview li.preview-item,body.settings_page_sharing ul.preview li.preview-item a{cursor:default;text-decoration:none}div.sd-social-icon .inner li.preview-item a span,div.sd-social-icon ul.preview li.preview-item a span{display:none}div.sd-social-icon ul.preview li.preview-item.preview-custom a span{display:inline-block}.services .preview li.share-custom a{text-decoration:none}.services ul li.end-fix{clear:both;float:none;visibility:hidden;padding:0;margin:0;height:20px;width:0}#enabled-services h2{font-size:20px;padding-top:0;font-weight:400!important;color:#999}body.settings_page_sharing #live-preview h2{font-size:20px;font-weight:400!important;color:#e3e3e3}body.settings_page_sharing .clearing{clear:both}body.settings_page_sharing .options .options-left{float:left}body.settings_page_sharing .input label{font-size:11px;line-height:16px}body.settings_page_sharing .advanced-form{padding:10px 14px 8px 10px;margin-left:-24px;display:none;border-top:1px #e3e3e3 solid;margin-top:4px}body.settings_page_sharing .utility{float:right;padding-top:10px;padding-right:10px;font-size:10px}body.settings_page_sharing .advanced input[type=submit]{float:left;margin-top:10px;margin-right:10px}.services li.dropzone{border:1px dashed #999;border-radius:3px;background:#e3e3e3;margin-right:10px;padding:5px;height:18px}.advanced-form .form-table th{width:auto!important}.advanced-form .button-secondary{margin-top:0!important}#hidden-drop-target{background:#e1e1e1;border:1px solid #cdcdcd;width:29%;padding:10px;vertical-align:top}#hidden-drop-target p{font-size:13px;font-style:italic;margin:0 0 10px 0}.preview li.preview-item{background-position:0 5px;cursor:default}.preview .option-smart-on{margin:3px 5px 0 0}.preview-digg .option-smart-on{background:url(images/smart-digg.png) no-repeat top left;background-size:76px 17px;width:76px;height:17px;margin-top:2px}.preview-reddit .option-smart-on{background:url(images/smart-reddit.png) no-repeat top left;background-size:104px 21px;width:104px;height:21px}.preview-facebook .option-smart-on{background:url(images/smart-like.png) no-repeat top left;background-size:85px 20px;width:85px;height:20px}.preview-twitter .option-smart-on{background:url(images/smart-twitter.png?1) no-repeat top left;background-size:60px 20px;width:60px;height:20px}.preview-linkedin .option-smart-on{background:url(images/linkedin-smart.png) no-repeat top center;background-size:99px 18px;width:99px;height:20px}.preview-tumblr .option-smart-on{background:url(images/smart-tumblr.png) no-repeat top left;background-size:62px 20px;width:62px;height:20px}.preview-pinterest .option-smart-on{background:url(images/smart-pinterest.png) no-repeat top left;background-size:39px 20px;width:39px;height:20px}.preview-pocket .option-smart-on{background:url(images/smart-pocket.png) no-repeat top left;background-size:60px 20px;width:60px;height:20px}.preview-skype .option-smart-on{background:url(images/smart-skype.png) no-repeat top left;background-size:60px 20px;width:60px;height:20px}.preview-item.share-deprecated{opacity:.5}.preview-item.share-deprecated a span{text-decoration:line-through}@media (-webkit-min-device-pixel-ratio:1.25),(min-resolution:120dpi){.preview-digg .option-smart-on{background-image:url(images/smart-digg@2x.png)}.preview-reddit .option-smart-on{background-image:url(images/smart-reddit@2x.png)}.preview-facebook .option-smart-on{background-image:url(images/smart-like@2x.png)}.preview-twitter .option-smart-on{background-image:url(images/smart-twitter@2x.png?1)}.preview-linkedin .option-smart-on{background-image:url(images/linkedin-smart@2x.png)}.preview-tumblr .option-smart-on{background-image:url(images/smart-tumblr@2x.png)}.preview-pinterest .option-smart-on{background-image:url(images/smart-pinterest@2x.png)}.preview-pocket .option-smart-on{background-image:url(images/smart-pocket@2x.png)}.preview-skype .option-smart-on{background-image:url(images/smart-skype@2x.png)}}.services .sharing-hidden li{background-color:transparent}.sharing-hidden li.share-end{clear:both;height:0;padding:0!important;margin:0!important;width:0;visibility:hidden;float:none}.preview .sharing-label{font-weight:700;border:0;padding:4px 6px 0 0}#services-config a.remove{background:#ddd;color:#fff;padding:0 4px 2px;border-radius:15px;-moz-border-radius:15px;-webkit-border-radius:15px;text-decoration:none;font-weight:700;font-size:10px}#services-config a.remove:hover{background:red}.sd-social-icon .inner a.sd-button span,.sd-social-icon .inner a.share-icon span{display:inline-block;overflow:hidden;width:0;text-indent:100%} \ No newline at end of file
diff --git a/plugins/jetpack/modules/sharedaddy/images/after-the-deadline@2x.png b/plugins/jetpack/modules/sharedaddy/images/after-the-deadline@2x.png
new file mode 100644
index 00000000..d06bd260
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/after-the-deadline@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/comments@2x.png b/plugins/jetpack/modules/sharedaddy/images/comments@2x.png
new file mode 100644
index 00000000..f1c8fbf9
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/comments@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/contact-form@2x.png b/plugins/jetpack/modules/sharedaddy/images/contact-form@2x.png
new file mode 100644
index 00000000..2c38752c
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/contact-form@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/custom.png b/plugins/jetpack/modules/sharedaddy/images/custom.png
new file mode 100644
index 00000000..46adefa4
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/custom.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/custom@2x.png b/plugins/jetpack/modules/sharedaddy/images/custom@2x.png
new file mode 100644
index 00000000..9bbfcfb6
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/custom@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/designfloat.png b/plugins/jetpack/modules/sharedaddy/images/designfloat.png
new file mode 100644
index 00000000..e2110bcc
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/designfloat.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/digg.png b/plugins/jetpack/modules/sharedaddy/images/digg.png
new file mode 100644
index 00000000..dc98382c
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/digg.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/digg@2x.png b/plugins/jetpack/modules/sharedaddy/images/digg@2x.png
new file mode 100644
index 00000000..f9bd8a74
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/digg@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/divider.png b/plugins/jetpack/modules/sharedaddy/images/divider.png
new file mode 100644
index 00000000..00f427ad
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/divider.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/divider@2x.png b/plugins/jetpack/modules/sharedaddy/images/divider@2x.png
new file mode 100644
index 00000000..901cf653
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/divider@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/draggy.png b/plugins/jetpack/modules/sharedaddy/images/draggy.png
new file mode 100644
index 00000000..b4633d29
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/draggy.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/draggy@2x.png b/plugins/jetpack/modules/sharedaddy/images/draggy@2x.png
new file mode 100644
index 00000000..133483ad
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/draggy@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/email.png b/plugins/jetpack/modules/sharedaddy/images/email.png
new file mode 100644
index 00000000..6753619a
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/email.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/email@2x.png b/plugins/jetpack/modules/sharedaddy/images/email@2x.png
new file mode 100644
index 00000000..6a6f70d2
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/email@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/ember.png b/plugins/jetpack/modules/sharedaddy/images/ember.png
new file mode 100644
index 00000000..47461ece
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/ember.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/enhanced-distribution@2x.png b/plugins/jetpack/modules/sharedaddy/images/enhanced-distribution@2x.png
new file mode 100644
index 00000000..694dba27
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/enhanced-distribution@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/facebook.png b/plugins/jetpack/modules/sharedaddy/images/facebook.png
new file mode 100644
index 00000000..91d3702f
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/facebook.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/facebook@2x.png b/plugins/jetpack/modules/sharedaddy/images/facebook@2x.png
new file mode 100644
index 00000000..10b36803
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/facebook@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/feed.png b/plugins/jetpack/modules/sharedaddy/images/feed.png
new file mode 100644
index 00000000..9eeeffd3
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/feed.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/icon-facebook-2x.png b/plugins/jetpack/modules/sharedaddy/images/icon-facebook-2x.png
new file mode 100644
index 00000000..3f2723fa
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/icon-facebook-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/icon-facebook.png b/plugins/jetpack/modules/sharedaddy/images/icon-facebook.png
new file mode 100644
index 00000000..bb9edab4
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/icon-facebook.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/icon-twitter-2x.png b/plugins/jetpack/modules/sharedaddy/images/icon-twitter-2x.png
new file mode 100644
index 00000000..a9a90141
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/icon-twitter-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/icon-twitter.png b/plugins/jetpack/modules/sharedaddy/images/icon-twitter.png
new file mode 100644
index 00000000..ec41046e
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/icon-twitter.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/icon-wordpress-2x.png b/plugins/jetpack/modules/sharedaddy/images/icon-wordpress-2x.png
new file mode 100644
index 00000000..23439d93
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/icon-wordpress-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/icon-wordpress.png b/plugins/jetpack/modules/sharedaddy/images/icon-wordpress.png
new file mode 100644
index 00000000..1ff384a0
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/icon-wordpress.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/kindle.png b/plugins/jetpack/modules/sharedaddy/images/kindle.png
new file mode 100644
index 00000000..a8b235c6
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/kindle.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/kindle@2x.png b/plugins/jetpack/modules/sharedaddy/images/kindle@2x.png
new file mode 100644
index 00000000..dfbcbec3
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/kindle@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/linkedin-horizontal.png b/plugins/jetpack/modules/sharedaddy/images/linkedin-horizontal.png
new file mode 100644
index 00000000..a55a3e60
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/linkedin-horizontal.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/linkedin-horizontal@2x.png b/plugins/jetpack/modules/sharedaddy/images/linkedin-horizontal@2x.png
new file mode 100644
index 00000000..cd400275
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/linkedin-horizontal@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/linkedin-nocount.png b/plugins/jetpack/modules/sharedaddy/images/linkedin-nocount.png
new file mode 100644
index 00000000..a5d4baeb
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/linkedin-nocount.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/linkedin-nocount@2x.png b/plugins/jetpack/modules/sharedaddy/images/linkedin-nocount@2x.png
new file mode 100644
index 00000000..ad599e67
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/linkedin-nocount@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/linkedin-smart.png b/plugins/jetpack/modules/sharedaddy/images/linkedin-smart.png
new file mode 100644
index 00000000..a55a3e60
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/linkedin-smart.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/linkedin-smart@2x.png b/plugins/jetpack/modules/sharedaddy/images/linkedin-smart@2x.png
new file mode 100644
index 00000000..cd400275
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/linkedin-smart@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/linkedin-vertical.png b/plugins/jetpack/modules/sharedaddy/images/linkedin-vertical.png
new file mode 100644
index 00000000..24bc27d4
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/linkedin-vertical.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/linkedin-vertical@2x.png b/plugins/jetpack/modules/sharedaddy/images/linkedin-vertical@2x.png
new file mode 100644
index 00000000..3e6216e5
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/linkedin-vertical@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/linkedin.png b/plugins/jetpack/modules/sharedaddy/images/linkedin.png
new file mode 100644
index 00000000..ee860f7f
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/linkedin.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/linkedin@2x.png b/plugins/jetpack/modules/sharedaddy/images/linkedin@2x.png
new file mode 100644
index 00000000..7139d05f
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/linkedin@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/loading.gif b/plugins/jetpack/modules/sharedaddy/images/loading.gif
new file mode 100644
index 00000000..85b99d46
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/loading.gif
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/more.png b/plugins/jetpack/modules/sharedaddy/images/more.png
new file mode 100644
index 00000000..eb5bb625
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/more.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/more@2x.png b/plugins/jetpack/modules/sharedaddy/images/more@2x.png
new file mode 100644
index 00000000..931e9caf
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/more@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/pinterest.png b/plugins/jetpack/modules/sharedaddy/images/pinterest.png
new file mode 100644
index 00000000..d170d748
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/pinterest.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/pinterest@2x.png b/plugins/jetpack/modules/sharedaddy/images/pinterest@2x.png
new file mode 100644
index 00000000..5229524f
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/pinterest@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/pocket.png b/plugins/jetpack/modules/sharedaddy/images/pocket.png
new file mode 100644
index 00000000..cba4e662
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/pocket.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/pocket@2x.png b/plugins/jetpack/modules/sharedaddy/images/pocket@2x.png
new file mode 100644
index 00000000..2512c887
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/pocket@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/print.png b/plugins/jetpack/modules/sharedaddy/images/print.png
new file mode 100644
index 00000000..71fa6bf6
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/print.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/print@2x.png b/plugins/jetpack/modules/sharedaddy/images/print@2x.png
new file mode 100644
index 00000000..bb6b4027
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/print@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/reddit.png b/plugins/jetpack/modules/sharedaddy/images/reddit.png
new file mode 100644
index 00000000..d6644565
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/reddit.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/reddit@2x.png b/plugins/jetpack/modules/sharedaddy/images/reddit@2x.png
new file mode 100644
index 00000000..11a3f2c3
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/reddit@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/rss.png b/plugins/jetpack/modules/sharedaddy/images/rss.png
new file mode 100644
index 00000000..7c92968f
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/rss.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/rss@2x.png b/plugins/jetpack/modules/sharedaddy/images/rss@2x.png
new file mode 100644
index 00000000..f007bf2e
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/rss@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/share-bg.png b/plugins/jetpack/modules/sharedaddy/images/share-bg.png
new file mode 100644
index 00000000..03c2d2bd
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/share-bg.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/sharing-hidden.png b/plugins/jetpack/modules/sharedaddy/images/sharing-hidden.png
new file mode 100644
index 00000000..3458c7bd
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/sharing-hidden.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/sharing-hidden@2x.png b/plugins/jetpack/modules/sharedaddy/images/sharing-hidden@2x.png
new file mode 100644
index 00000000..5924c4af
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/sharing-hidden@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-digg.png b/plugins/jetpack/modules/sharedaddy/images/smart-digg.png
new file mode 100644
index 00000000..6f564b7d
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-digg.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-digg@2x.png b/plugins/jetpack/modules/sharedaddy/images/smart-digg@2x.png
new file mode 100644
index 00000000..6468a53e
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-digg@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-facebook.png b/plugins/jetpack/modules/sharedaddy/images/smart-facebook.png
new file mode 100644
index 00000000..799e0986
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-facebook.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-facebook@2x.png b/plugins/jetpack/modules/sharedaddy/images/smart-facebook@2x.png
new file mode 100644
index 00000000..bc277c2f
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-facebook@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-like.png b/plugins/jetpack/modules/sharedaddy/images/smart-like.png
new file mode 100644
index 00000000..368a6c11
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-like.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-like@2x.png b/plugins/jetpack/modules/sharedaddy/images/smart-like@2x.png
new file mode 100644
index 00000000..7ad4d638
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-like@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-pinterest.png b/plugins/jetpack/modules/sharedaddy/images/smart-pinterest.png
new file mode 100644
index 00000000..ac78d5ad
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-pinterest.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-pinterest@2x.png b/plugins/jetpack/modules/sharedaddy/images/smart-pinterest@2x.png
new file mode 100644
index 00000000..ea03b942
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-pinterest@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-pocket.png b/plugins/jetpack/modules/sharedaddy/images/smart-pocket.png
new file mode 100644
index 00000000..4a368a35
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-pocket.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-pocket@2x.png b/plugins/jetpack/modules/sharedaddy/images/smart-pocket@2x.png
new file mode 100644
index 00000000..b84a0d90
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-pocket@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-reddit.png b/plugins/jetpack/modules/sharedaddy/images/smart-reddit.png
new file mode 100644
index 00000000..5afa0aa6
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-reddit.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-reddit@2x.png b/plugins/jetpack/modules/sharedaddy/images/smart-reddit@2x.png
new file mode 100644
index 00000000..da4b569b
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-reddit@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-skype.png b/plugins/jetpack/modules/sharedaddy/images/smart-skype.png
new file mode 100644
index 00000000..b800d516
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-skype.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-skype@2x.png b/plugins/jetpack/modules/sharedaddy/images/smart-skype@2x.png
new file mode 100644
index 00000000..6828be4a
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-skype@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-stumbleupon.png b/plugins/jetpack/modules/sharedaddy/images/smart-stumbleupon.png
new file mode 100644
index 00000000..922d84b9
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-stumbleupon.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-stumbleupon@2x.png b/plugins/jetpack/modules/sharedaddy/images/smart-stumbleupon@2x.png
new file mode 100644
index 00000000..a807aef8
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-stumbleupon@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-tumblr.png b/plugins/jetpack/modules/sharedaddy/images/smart-tumblr.png
new file mode 100644
index 00000000..147975e1
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-tumblr.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-tumblr@2x.png b/plugins/jetpack/modules/sharedaddy/images/smart-tumblr@2x.png
new file mode 100644
index 00000000..b73b9a26
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-tumblr@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-twitter.png b/plugins/jetpack/modules/sharedaddy/images/smart-twitter.png
new file mode 100644
index 00000000..299c7d88
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-twitter.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/smart-twitter@2x.png b/plugins/jetpack/modules/sharedaddy/images/smart-twitter@2x.png
new file mode 100644
index 00000000..0be96c46
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/smart-twitter@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/tumblr.png b/plugins/jetpack/modules/sharedaddy/images/tumblr.png
new file mode 100644
index 00000000..d248cd09
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/tumblr.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/tumblr@2x.png b/plugins/jetpack/modules/sharedaddy/images/tumblr@2x.png
new file mode 100644
index 00000000..f991236c
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/tumblr@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/twitter.png b/plugins/jetpack/modules/sharedaddy/images/twitter.png
new file mode 100644
index 00000000..ec41046e
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/twitter.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/twitter@2x.png b/plugins/jetpack/modules/sharedaddy/images/twitter@2x.png
new file mode 100644
index 00000000..aa666e66
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/twitter@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/wordpress.png b/plugins/jetpack/modules/sharedaddy/images/wordpress.png
new file mode 100644
index 00000000..94e92823
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/wordpress.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/images/wordpress@2x.png b/plugins/jetpack/modules/sharedaddy/images/wordpress@2x.png
new file mode 100644
index 00000000..2da25ffc
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/images/wordpress@2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/sharedaddy/recaptcha.php b/plugins/jetpack/modules/sharedaddy/recaptcha.php
new file mode 100644
index 00000000..58ae6563
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/recaptcha.php
@@ -0,0 +1,188 @@
+<?php
+
+/**
+ * Class that handles reCAPTCHA.
+ */
+class Jetpack_ReCaptcha {
+
+ /**
+ * URL to which requests are POSTed.
+ *
+ * @const string
+ */
+ const VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
+
+ /**
+ * Site key to use in HTML code.
+ *
+ * @var string
+ */
+ private $site_key;
+
+ /**
+ * Shared secret for the site.
+ *
+ * @var string
+ */
+ private $secret_key;
+
+ /**
+ * Config for reCAPTCHA instance.
+ *
+ * @var array
+ */
+ private $config;
+
+ /**
+ * Error codes returned from reCAPTCHA API.
+ *
+ * @see https://developers.google.com/recaptcha/docs/verify
+ *
+ * @var array
+ */
+ private $error_codes;
+
+ /**
+ * Create a configured instance to use the reCAPTCHA service.
+ *
+ * @param string $site_key Site key to use in HTML code.
+ * @param string $secret_key Shared secret between site and reCAPTCHA server.
+ * @param array $config Config array to optionally configure reCAPTCHA instance.
+ */
+ public function __construct( $site_key, $secret_key, $config = array() ) {
+ $this->site_key = $site_key;
+ $this->secret_key = $secret_key;
+ $this->config = wp_parse_args( $config, $this->get_default_config() );
+
+ $this->error_codes = array(
+ 'missing-input-secret' => __( 'The secret parameter is missing', 'jetpack' ),
+ 'invalid-input-secret' => __( 'The secret parameter is invalid or malformed', 'jetpack' ),
+ 'missing-input-response' => __( 'The response parameter is missing', 'jetpack' ),
+ 'invalid-input-response' => __( 'The response parameter is invalid or malformed', 'jetpack' ),
+ 'invalid-json' => __( 'Invalid JSON', 'jetpack' ),
+ 'unexpected-response' => __( 'Unexpected response', 'jetpack' ),
+ 'unexpected-hostname' => __( 'Unexpected hostname', 'jetpack' ),
+ );
+ }
+
+ /**
+ * Get default config for this reCAPTCHA instance.
+ *
+ * @return array Default config
+ */
+ public function get_default_config() {
+ return array(
+ 'language' => get_locale(),
+ 'script_async' => true,
+ 'tag_class' => 'g-recaptcha',
+ 'tag_attributes' => array(
+ 'theme' => 'light',
+ 'type' => 'image',
+ 'tabindex' => 0,
+ ),
+ );
+ }
+
+ /**
+ * Calls the reCAPTCHA siteverify API to verify whether the user passes
+ * CAPTCHA test.
+ *
+ * @param string $response The value of 'g-recaptcha-response' in the submitted
+ * form.
+ * @param string $remote_ip The end user's IP address.
+ *
+ * @return bool|WP_Error Returns true if verified. Otherwise WP_Error is returned.
+ */
+ public function verify( $response, $remote_ip ) {
+ // No need make a request if response is empty.
+ if ( empty( $response ) ) {
+ return new WP_Error( 'missing-input-response', $this->error_codes['missing-input-response'], 400 );
+ }
+
+ $resp = wp_remote_post( self::VERIFY_URL, $this->get_verify_request_params( $response, $remote_ip ) );
+ if ( is_wp_error( $resp ) ) {
+ return $resp;
+ }
+
+ $resp_decoded = json_decode( wp_remote_retrieve_body( $resp ), true );
+ if ( ! $resp_decoded ) {
+ return new WP_Error( 'invalid-json', $this->error_codes['invalid-json'], 400 );
+ }
+
+ // Default error code and message.
+ $error_code = 'unexpected-response';
+ $error_message = $this->error_codes['unexpected-response'];
+
+ // Use the first error code if exists.
+ if ( isset( $resp_decoded['error-codes'] ) && is_array( $resp_decoded['error-codes'] ) ) {
+ if ( isset( $resp_decoded['error-codes'][0] ) && isset( $this->error_codes[ $resp_decoded['error-codes'][0] ] ) ) {
+ $error_message = $this->error_codes[ $resp_decoded['error-codes'][0] ];
+ $error_code = $resp_decoded['error-codes'][0];
+ }
+ }
+
+ if ( ! isset( $resp_decoded['success'] ) ) {
+ return new WP_Error( $error_code, $error_message );
+ }
+
+ if ( true !== $resp_decoded['success'] ) {
+ return new WP_Error( $error_code, $error_message );
+ }
+
+ // Validate the hostname matches expected source
+ if ( isset( $resp_decoded['hostname'] ) ) {
+ $url = wp_parse_url( get_home_url() );
+ if ( $url['host'] !== $resp_decoded['hostname'] ) {
+ return new WP_Error( 'unexpected-host', $this->error_codes['unexpected-hostname'] );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get siteverify request parameters.
+ *
+ * @param string $response The value of 'g-recaptcha-response' in the submitted
+ * form.
+ * @param string $remote_ip The end user's IP address.
+ *
+ * @return array
+ */
+ public function get_verify_request_params( $response, $remote_ip ) {
+ return array(
+ 'body' => array(
+ 'secret' => $this->secret_key,
+ 'response' => $response,
+ 'remoteip' => $remote_ip,
+ ),
+ 'sslverify' => true,
+ );
+ }
+
+ /**
+ * Get reCAPTCHA HTML to render.
+ *
+ * @return string
+ */
+ public function get_recaptcha_html() {
+ return sprintf(
+ '
+ <div
+ class="%s"
+ data-sitekey="%s"
+ data-theme="%s"
+ data-type="%s"
+ data-tabindex="%s"></div>
+ <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?hl=%s"%s></script>
+ ',
+ esc_attr( $this->config['tag_class'] ),
+ esc_attr( $this->site_key ),
+ esc_attr( $this->config['tag_attributes']['theme'] ),
+ esc_attr( $this->config['tag_attributes']['type'] ),
+ esc_attr( $this->config['tag_attributes']['tabindex'] ),
+ rawurlencode( $this->config['language'] ),
+ $this->config['script_async'] ? ' async' : ''
+ );
+ }
+}
diff --git a/plugins/jetpack/modules/sharedaddy/sharedaddy.php b/plugins/jetpack/modules/sharedaddy/sharedaddy.php
new file mode 100644
index 00000000..1e5de22c
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/sharedaddy.php
@@ -0,0 +1,289 @@
+<?php
+/*
+Plugin Name: Sharedaddy
+Description: The most super duper sharing tool on the interwebs.
+Version: 0.3.1
+Author: Automattic, Inc.
+Author URI: http://automattic.com/
+Plugin URI: http://en.blog.wordpress.com/2010/08/24/more-ways-to-share/
+*/
+
+require_once plugin_dir_path( __FILE__ ).'sharing.php';
+
+function sharing_email_send_post( $data ) {
+
+ $content = sharing_email_send_post_content( $data );
+ // Borrowed from wp_mail();
+ $sitename = strtolower( $_SERVER['SERVER_NAME'] );
+ if ( substr( $sitename, 0, 4 ) == 'www.' ) {
+ $sitename = substr( $sitename, 4 );
+ }
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $from_email = apply_filters( 'wp_mail_from', 'wordpress@' . $sitename );
+
+ if ( ! empty( $data['name'] ) ) {
+ $s_name = (string) $data['name'];
+ $name_needs_encoding_regex =
+ '/[' .
+ // SpamAssasin's list of characters which "need MIME" encoding
+ '\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff' .
+ // Our list of "unsafe" characters
+ '<\r\n' .
+ ']/';
+
+ $needs_encoding =
+ // If it contains any blacklisted chars,
+ preg_match( $name_needs_encoding_regex, $s_name ) ||
+ // Or if we can't use `mb_convert_encoding`
+ ! function_exists( 'mb_convert_encoding' ) ||
+ // Or if it's not already ASCII
+ mb_convert_encoding( $data['name'], 'ASCII' ) !== $s_name;
+
+ if ( $needs_encoding ) {
+ $data['name'] = sprintf( '=?UTF-8?B?%s?=', base64_encode( $data['name'] ) );
+ }
+ }
+
+ $headers[] = sprintf( 'From: %1$s <%2$s>', $data['name'], $from_email );
+ $headers[] = sprintf( 'Reply-To: %1$s <%2$s>', $data['name'], $data['source'] );
+
+ // Make sure to pass the title through the normal sharing filters.
+ $title = $data['sharing_source']->get_share_title( $data['post']->ID );
+
+ /**
+ * Filter the Sharing Email Send Post Subject.
+ *
+ * @module sharedaddy
+ *
+ * @since 5.8.0
+ *
+ * @param string $var Sharing Email Send Post Subject. Default is "Shared Post".
+ */
+ $subject = apply_filters( 'wp_sharing_email_send_post_subject', '[' . __( 'Shared Post', 'jetpack' ) . '] ' . $title );
+
+ wp_mail( $data['target'], $subject, $content, $headers );
+}
+
+
+/* Checks for spam using akismet if available. */
+/* Return $data as it if email about to be send out is not spam. */
+function sharing_email_check_for_spam_via_akismet( $data ) {
+
+ if ( ! Jetpack::is_akismet_active() )
+ return $data;
+
+ // Prepare the body_request for akismet
+ $body_request = array(
+ 'blog' => get_option( 'home' ),
+ 'permalink' => $data['sharing_source']->get_share_url( $data['post']->ID ),
+ 'comment_type' => 'share',
+ 'comment_author' => $data['name'],
+ 'comment_author_email' => $data['source'],
+ 'comment_content' => sharing_email_send_post_content( $data ),
+ 'user_agent' => ( isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null ),
+ );
+
+ if ( method_exists( 'Akismet', 'http_post' ) ) {
+ $body_request['user_ip'] = Akismet::get_ip_address();
+ $response = Akismet::http_post( build_query( $body_request ), 'comment-check' );
+ } else {
+ global $akismet_api_host, $akismet_api_port;
+ $body_request['user_ip'] = ( isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null );
+ $response = akismet_http_post( build_query( $body_request ), $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
+ }
+
+ // The Response is spam lets not send the email.
+ if ( ! empty( $response ) && isset( $response[1] ) && 'true' == trim( $response[1] ) ) { // 'true' is spam
+ return false; // don't send the email
+ }
+ return $data;
+}
+
+function sharing_email_send_post_content( $data ) {
+ /* translators: included in email when post is shared via email. First item is sender's name. Second is sender's email address. */
+ $content = sprintf( __( '%1$s (%2$s) thinks you may be interested in the following post:', 'jetpack' ), $data['name'], $data['source'] );
+ $content .= "\n\n";
+ // Make sure to pass the title and URL through the normal sharing filters.
+ $content .= $data['sharing_source']->get_share_title( $data['post']->ID ) . "\n";
+ $content .= $data['sharing_source']->get_share_url( $data['post']->ID ) . "\n";
+ return $content;
+}
+
+function sharing_add_meta_box() {
+ global $post;
+ if ( empty( $post ) ) { // If a current post is not defined, such as when editing a comment.
+ return;
+ }
+
+ /**
+ * Filter whether to display the Sharing Meta Box or not.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.8.0
+ *
+ * @param bool true Display Sharing Meta Box.
+ * @param $post Post.
+ */
+ if ( ! apply_filters( 'sharing_meta_box_show', true, $post ) ) {
+ return;
+ }
+
+ $post_types = get_post_types( array( 'public' => true ) );
+ /**
+ * Filter the Sharing Meta Box title.
+ *
+ * @module sharedaddy
+ *
+ * @since 2.2.0
+ *
+ * @param string $var Sharing Meta Box title. Default is "Sharing".
+ */
+ $title = apply_filters( 'sharing_meta_box_title', __( 'Sharing', 'jetpack' ) );
+ if ( $post->ID !== get_option( 'page_for_posts' ) ) {
+ foreach( $post_types as $post_type ) {
+ add_meta_box( 'sharing_meta', $title, 'sharing_meta_box_content', $post_type, 'side', 'default', array( '__back_compat_meta_box' => true ) );
+ }
+ }
+}
+
+
+function sharing_meta_box_content( $post ) {
+ /**
+ * Fires before the sharing meta box content.
+ *
+ * @module sharedaddy
+ *
+ * @since 2.2.0
+ *
+ * @param WP_Post $post The post to share.
+ */
+ do_action( 'start_sharing_meta_box_content', $post );
+
+ $disabled = get_post_meta( $post->ID, 'sharing_disabled', true ); ?>
+
+ <p>
+ <label for="enable_post_sharing">
+ <input type="checkbox" name="enable_post_sharing" id="enable_post_sharing" value="1" <?php checked( !$disabled ); ?>>
+ <?php _e( 'Show sharing buttons.' , 'jetpack'); ?>
+ </label>
+ <input type="hidden" name="sharing_status_hidden" value="1" />
+ </p>
+
+ <?php
+ /**
+ * Fires after the sharing meta box content.
+ *
+ * @module sharedaddy
+ *
+ * @since 2.2.0
+ *
+ * @param WP_Post $post The post to share.
+ */
+ do_action( 'end_sharing_meta_box_content', $post );
+}
+
+function sharing_meta_box_save( $post_id ) {
+ if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE )
+ return $post_id;
+
+ // Record sharing disable
+ if ( isset( $_POST['post_type'] ) && ( $post_type_object = get_post_type_object( $_POST['post_type'] ) ) && $post_type_object->public ) {
+ if ( current_user_can( 'edit_post', $post_id ) ) {
+ if ( isset( $_POST['sharing_status_hidden'] ) ) {
+ if ( !isset( $_POST['enable_post_sharing'] ) ) {
+ update_post_meta( $post_id, 'sharing_disabled', 1 );
+ } else {
+ delete_post_meta( $post_id, 'sharing_disabled' );
+ }
+ }
+ }
+ }
+
+ return $post_id;
+}
+
+function sharing_meta_box_protected( $protected, $meta_key, $meta_type ) {
+ if ( 'sharing_disabled' == $meta_key )
+ $protected = true;
+
+ return $protected;
+}
+
+add_filter( 'is_protected_meta', 'sharing_meta_box_protected', 10, 3 );
+
+function sharing_plugin_settings( $links ) {
+ $settings_link = '<a href="options-general.php?page=sharing.php">'.__( 'Settings', 'jetpack' ).'</a>';
+ array_unshift( $links, $settings_link );
+ return $links;
+}
+
+function sharing_add_plugin_settings($links, $file) {
+ if ( $file == basename( dirname( __FILE__ ) ).'/'.basename( __FILE__ ) ) {
+ $links[] = '<a href="options-general.php?page=sharing.php">' . __( 'Settings', 'jetpack' ) . '</a>';
+ $links[] = '<a href="http://support.wordpress.com/sharing/" rel="noopener noreferrer" target="_blank">' . __( 'Support', 'jetpack' ) . '</a>';
+ }
+
+ return $links;
+}
+
+function sharing_init() {
+ if ( Jetpack_Options::get_option_and_ensure_autoload( 'sharedaddy_disable_resources', '0' ) ) {
+ add_filter( 'sharing_js', 'sharing_disable_js' );
+ remove_action( 'wp_head', 'sharing_add_header', 1 );
+ }
+}
+
+function sharing_disable_js() {
+ return false;
+}
+
+function sharing_global_resources() {
+ $disable = get_option( 'sharedaddy_disable_resources' );
+?>
+<tr valign="top">
+ <th scope="row"><label for="disable_css"><?php _e( 'Disable CSS and JS', 'jetpack' ); ?></label></th>
+ <td>
+ <input id="disable_css" type="checkbox" name="disable_resources" <?php if ( $disable == 1 ) echo ' checked="checked"'; ?>/> <small><em><?php _e( 'Advanced. If this option is checked, you must include these files in your theme manually for the sharing links to work.', 'jetpack' ); ?></em></small>
+ </td>
+</tr>
+<?php
+}
+
+function sharing_global_resources_save() {
+ update_option( 'sharedaddy_disable_resources', isset( $_POST['disable_resources'] ) ? 1 : 0 );
+}
+
+function sharing_email_dialog() {
+ require_once plugin_dir_path( __FILE__ ) . 'recaptcha.php';
+
+ $recaptcha = new Jetpack_ReCaptcha( RECAPTCHA_PUBLIC_KEY, RECAPTCHA_PRIVATE_KEY );
+ echo $recaptcha->get_recaptcha_html(); // xss ok
+}
+
+function sharing_email_check( $true, $post, $data ) {
+ require_once plugin_dir_path( __FILE__ ) . 'recaptcha.php';
+
+ $recaptcha = new Jetpack_ReCaptcha( RECAPTCHA_PUBLIC_KEY, RECAPTCHA_PRIVATE_KEY );
+ $response = ! empty( $_POST['g-recaptcha-response'] ) ? $_POST['g-recaptcha-response'] : '';
+ $result = $recaptcha->verify( $response, $_SERVER['REMOTE_ADDR'] );
+
+ return ( true === $result );
+}
+
+add_action( 'init', 'sharing_init' );
+add_action( 'add_meta_boxes', 'sharing_add_meta_box' );
+add_action( 'save_post', 'sharing_meta_box_save' );
+add_action( 'edit_attachment', 'sharing_meta_box_save' );
+add_action( 'sharing_email_send_post', 'sharing_email_send_post' );
+add_filter( 'sharing_email_can_send', 'sharing_email_check_for_spam_via_akismet' );
+add_action( 'sharing_global_options', 'sharing_global_resources', 30 );
+add_action( 'sharing_admin_update', 'sharing_global_resources_save' );
+add_action( 'plugin_action_links_'.basename( dirname( __FILE__ ) ).'/'.basename( __FILE__ ), 'sharing_plugin_settings', 10, 4 );
+add_filter( 'plugin_row_meta', 'sharing_add_plugin_settings', 10, 2 );
+
+if ( defined( 'RECAPTCHA_PUBLIC_KEY' ) && defined( 'RECAPTCHA_PRIVATE_KEY' ) ) {
+ add_action( 'sharing_email_dialog', 'sharing_email_dialog' );
+ add_filter( 'sharing_email_check', 'sharing_email_check', 10, 3 );
+}
diff --git a/plugins/jetpack/modules/sharedaddy/sharing-service.php b/plugins/jetpack/modules/sharedaddy/sharing-service.php
new file mode 100644
index 00000000..7e453262
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/sharing-service.php
@@ -0,0 +1,955 @@
+<?php
+
+include_once dirname( __FILE__ ) . '/sharing-sources.php';
+
+define( 'WP_SHARING_PLUGIN_VERSION', JETPACK__VERSION );
+
+class Sharing_Service {
+ private $global = false;
+ public $default_sharing_label = '';
+
+ public function __construct() {
+ $this->default_sharing_label = __( 'Share this:', 'jetpack' );
+ }
+
+ /**
+ * Gets a generic list of all services, without any config
+ */
+ public function get_all_services_blog() {
+ $options = get_option( 'sharing-options' );
+
+ $all = $this->get_all_services();
+ $services = array();
+
+ foreach ( $all as $id => $name ) {
+ if ( isset( $all[ $id ] ) ) {
+ $config = array();
+
+ // Pre-load custom modules otherwise they won't know who they are
+ if ( substr( $id, 0, 7 ) == 'custom-' && is_array( $options[ $id ] ) ) {
+ $config = $options[ $id ];
+ }
+
+ $services[ $id ] = new $all[ $id ]( $id, $config );
+ }
+ }
+
+ return $services;
+ }
+
+ /**
+ * Gets a list of all available service names and classes
+ */
+ public function get_all_services( $include_custom = true ) {
+ // Default services
+ // if you update this list, please update the REST API tests
+ // in bin/tests/api/suites/SharingTest.php
+ $services = array(
+ 'print' => 'Share_Print',
+ 'facebook' => 'Share_Facebook',
+ 'linkedin' => 'Share_LinkedIn',
+ 'reddit' => 'Share_Reddit',
+ 'twitter' => 'Share_Twitter',
+ 'tumblr' => 'Share_Tumblr',
+ 'pinterest' => 'Share_Pinterest',
+ 'pocket' => 'Share_Pocket',
+ 'telegram' => 'Share_Telegram',
+ 'jetpack-whatsapp' => 'Jetpack_Share_WhatsApp',
+ 'skype' => 'Share_Skype',
+ );
+
+ /**
+ * Filters if Email Sharing is enabled.
+ *
+ * E-Mail sharing is often problematic due to spam concerns, so this filter enables it to be quickly and simply toggled.
+ * @module sharedaddy
+ *
+ * @since 5.1.0
+ *
+ * @param bool $email Is e-mail sharing enabled? Default false if Akismet is not active or true if Akismet is active.
+ */
+ if ( apply_filters( 'sharing_services_email', Jetpack::is_akismet_active() ) ) {
+ $services['email'] = 'Share_Email';
+ }
+
+ if ( is_multisite() && is_plugin_active( 'press-this/press-this-plugin.php' ) ) {
+ $services['press-this'] = 'Share_PressThis';
+ }
+
+ if ( $include_custom ) {
+ // Add any custom services in
+ $options = $this->get_global_options();
+ foreach ( (array) $options['custom'] as $custom_id ) {
+ $services[ $custom_id ] = 'Share_Custom';
+ }
+ }
+
+ /**
+ * Filters the list of available Sharing Services.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param array $services Array of all available Sharing Services.
+ */
+ return apply_filters( 'sharing_services', $services );
+ }
+
+ public function new_service( $label, $url, $icon ) {
+ // Validate
+ $label = trim( wp_html_excerpt( wp_kses( $label, array() ), 30 ) );
+ $url = trim( esc_url_raw( $url ) );
+ $icon = trim( esc_url_raw( $icon ) );
+
+ if ( $label && $url && $icon ) {
+ $options = get_option( 'sharing-options' );
+ if ( ! is_array( $options ) ) {
+ $options = array();
+ }
+
+ $service_id = 'custom-' . time();
+
+ // Add a new custom service
+ $options['global']['custom'][] = $service_id;
+ if ( false !== $this->global ) {
+ $this->global['custom'][] = $service_id;
+ }
+
+ update_option( 'sharing-options', $options );
+
+ // Create a custom service and set the options for it
+ $service = new Share_Custom(
+ $service_id, array(
+ 'name' => $label,
+ 'url' => $url,
+ 'icon' => $icon,
+ )
+ );
+ $this->set_service( $service_id, $service );
+
+ // Return the service
+ return $service;
+ }
+
+ return false;
+ }
+
+ public function delete_service( $service_id ) {
+ $options = get_option( 'sharing-options' );
+ if ( isset( $options[ $service_id ] ) ) {
+ unset( $options[ $service_id ] );
+ }
+
+ $key = array_search( $service_id, $options['global']['custom'] );
+ if ( $key !== false ) {
+ unset( $options['global']['custom'][ $key ] );
+ }
+
+ update_option( 'sharing-options', $options );
+ return true;
+ }
+
+ public function set_blog_services( array $visible, array $hidden ) {
+ $services = $this->get_all_services();
+ // Validate the services
+ $available = array_keys( $services );
+
+ // Only allow services that we have defined
+ $hidden = array_intersect( $hidden, $available );
+ $visible = array_intersect( $visible, $available );
+
+ // Ensure we don't have the same ones in hidden and visible
+ $hidden = array_diff( $hidden, $visible );
+
+ /**
+ * Control the state of the list of sharing services.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param array $args {
+ * Array of options describing the state of the sharing services.
+ *
+ * @type array $services List of all available service names and classes.
+ * @type array $available Validated list of all available service names and classes.
+ * @type array $hidden List of services hidden behind a "More" button.
+ * @type array $visible List of visible services.
+ * @type array $this->get_blog_services() Array of Sharing Services currently enabled.
+ * }
+ */
+ do_action(
+ 'sharing_get_services_state', array(
+ 'services' => $services,
+ 'available' => $available,
+ 'hidden' => $hidden,
+ 'visible' => $visible,
+ 'currently_enabled' => $this->get_blog_services(),
+ )
+ );
+
+ return update_option(
+ 'sharing-services', array(
+ 'visible' => $visible,
+ 'hidden' => $hidden,
+ )
+ );
+ }
+
+ public function get_blog_services() {
+ $options = get_option( 'sharing-options' );
+ $enabled = get_option( 'sharing-services' );
+ $services = $this->get_all_services();
+
+ /**
+ * Check if options exist and are well formatted.
+ * This avoids issues on sites with corrupted options.
+ * @see https://github.com/Automattic/jetpack/issues/6121
+ */
+ if ( ! is_array( $options ) || ! isset( $options['button_style'], $options['global'] ) ) {
+ $global_options = array( 'global' => $this->get_global_options() );
+ $options = is_array( $options )
+ ? array_merge( $options, $global_options )
+ : $global_options;
+ }
+
+ $global = $options['global'];
+
+ // Default services
+ if ( ! is_array( $enabled ) ) {
+ $enabled = array(
+ 'visible' => array(),
+ 'hidden' => array(),
+ );
+
+ /**
+ * Filters the list of default Sharing Services.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param array $enabled Array of default Sharing Services.
+ */
+ $enabled = apply_filters( 'sharing_default_services', $enabled );
+ }
+
+ // Cleanup after any filters that may have produced duplicate services
+ if ( is_array( $enabled['visible'] ) ) {
+ $enabled['visible'] = array_unique( $enabled['visible'] );
+ } else {
+ $enabled['visible'] = array();
+ }
+
+ if ( is_array( $enabled['hidden'] ) ) {
+ $enabled['hidden'] = array_unique( $enabled['hidden'] );
+ } else {
+ $enabled['hidden'] = array();
+ }
+
+ // Form the enabled services
+ $blog = array(
+ 'visible' => array(),
+ 'hidden' => array(),
+ );
+
+ foreach ( $blog as $area => $stuff ) {
+ foreach ( (array) $enabled[ $area ] as $service ) {
+ if ( isset( $services[ $service ] ) ) {
+ if ( ! isset( $options[ $service ] ) || ! is_array( $options[ $service ] ) ) {
+ $options[ $service ] = array();
+ }
+ $blog[ $area ][ $service ] = new $services[ $service ]( $service, array_merge( $global, $options[ $service ] ) );
+ }
+ }
+ }
+
+ /**
+ * Filters the list of enabled Sharing Services.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param array $blog Array of enabled Sharing Services.
+ */
+ $blog = apply_filters( 'sharing_services_enabled', $blog );
+
+ // Add CSS for NASCAR
+ if ( count( $blog['visible'] ) || count( $blog['hidden'] ) ) {
+ add_filter( 'post_flair_block_css', 'post_flair_service_enabled_sharing' );
+ }
+
+ // Convenience for checking if a service is present
+ $blog['all'] = array_flip( array_merge( array_keys( $blog['visible'] ), array_keys( $blog['hidden'] ) ) );
+ return $blog;
+ }
+
+ public function get_service( $service_name ) {
+ $services = $this->get_blog_services();
+
+ if ( isset( $services['visible'][ $service_name ] ) ) {
+ return $services['visible'][ $service_name ];
+ }
+
+ if ( isset( $services['hidden'][ $service_name ] ) ) {
+ return $services['hidden'][ $service_name ];
+ }
+
+ return false;
+ }
+
+ public function set_global_options( $data ) {
+ $options = get_option( 'sharing-options' );
+
+ // No options yet
+ if ( ! is_array( $options ) ) {
+ $options = array();
+ }
+
+ // Defaults
+ $options['global'] = array(
+ 'button_style' => 'icon-text',
+ 'sharing_label' => $this->default_sharing_label,
+ 'open_links' => 'same',
+ 'show' => array(),
+ 'custom' => isset( $options['global']['custom'] ) ? $options['global']['custom'] : array(),
+ );
+
+ /**
+ * Filters global sharing settings.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param array $options['global'] Array of global sharing settings.
+ */
+ $options['global'] = apply_filters( 'sharing_default_global', $options['global'] );
+
+ // Validate options and set from our data
+ if ( isset( $data['button_style'] ) && in_array( $data['button_style'], array( 'icon-text', 'icon', 'text', 'official' ) ) ) {
+ $options['global']['button_style'] = $data['button_style'];
+ }
+
+ if ( isset( $data['sharing_label'] ) ) {
+ if ( $this->default_sharing_label === $data['sharing_label'] ) {
+ $options['global']['sharing_label'] = false;
+ } else {
+ $options['global']['sharing_label'] = trim( wp_kses( stripslashes( $data['sharing_label'] ), array() ) );
+ }
+ }
+
+ if ( isset( $data['open_links'] ) && in_array( $data['open_links'], array( 'new', 'same' ) ) ) {
+ $options['global']['open_links'] = $data['open_links'];
+ }
+
+ $shows = array_values( get_post_types( array( 'public' => true ) ) );
+ $shows[] = 'index';
+ if ( isset( $data['show'] ) ) {
+ if ( is_scalar( $data['show'] ) ) {
+ switch ( $data['show'] ) {
+ case 'posts':
+ $data['show'] = array( 'post', 'page' );
+ break;
+ case 'index':
+ $data['show'] = array( 'index' );
+ break;
+ case 'posts-index':
+ $data['show'] = array( 'post', 'page', 'index' );
+ break;
+ }
+ }
+
+ if ( $data['show'] = array_intersect( $data['show'], $shows ) ) {
+ $options['global']['show'] = $data['show'];
+ }
+ }
+
+ update_option( 'sharing-options', $options );
+ return $options['global'];
+ }
+
+ public function get_global_options() {
+ if ( $this->global === false ) {
+ $options = get_option( 'sharing-options' );
+
+ if ( is_array( $options ) && isset( $options['global'] ) && is_array( $options['global'] ) ) {
+ $this->global = $options['global'];
+ } else {
+ $this->global = $this->set_global_options( $options['global'] );
+ }
+ }
+
+ if ( ! isset( $this->global['show'] ) ) {
+ $this->global['show'] = array( 'post', 'page' );
+ } elseif ( is_scalar( $this->global['show'] ) ) {
+ switch ( $this->global['show'] ) {
+ case 'posts':
+ $this->global['show'] = array( 'post', 'page' );
+ break;
+ case 'index':
+ $this->global['show'] = array( 'index' );
+ break;
+ case 'posts-index':
+ $this->global['show'] = array( 'post', 'page', 'index' );
+ break;
+ }
+ }
+
+ if ( false === $this->global['sharing_label'] ) {
+ $this->global['sharing_label'] = $this->default_sharing_label;
+ }
+
+ return $this->global;
+ }
+
+ public function set_service( $id, Sharing_Source $service ) {
+ // Update the options for this service
+ $options = get_option( 'sharing-options' );
+
+ // No options yet
+ if ( ! is_array( $options ) ) {
+ $options = array();
+ }
+
+ /**
+ * Get the state of a sharing button.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param array $args {
+ * State of a sharing button.
+ *
+ * @type string $id Service ID.
+ * @type array $options Array of all sharing options.
+ * @type array $service Details about a service.
+ * }
+ */
+ do_action(
+ 'sharing_get_button_state', array(
+ 'id' => $id,
+ 'options' => $options,
+ 'service' => $service,
+ )
+ );
+
+ $options[ $id ] = $service->get_options();
+
+ update_option( 'sharing-options', array_filter( $options ) );
+ }
+
+ // Soon to come to a .org plugin near you!
+ public function get_total( $service_name = false, $post_id = false, $_blog_id = false ) {
+ global $wpdb, $blog_id;
+ if ( ! $_blog_id ) {
+ $_blog_id = $blog_id;
+ }
+ if ( $service_name == false ) {
+ if ( $post_id > 0 ) {
+ // total number of shares for this post
+ return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d AND post_id = %d', $_blog_id, $post_id ) );
+ } else {
+ // total number of shares for this blog
+ return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d', $_blog_id ) );
+ }
+ }
+
+ if ( $post_id > 0 ) {
+ return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d AND post_id = %d AND share_service = %s', $_blog_id, $post_id, $service_name ) );
+ } else {
+ return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d AND share_service = %s', $_blog_id, $service_name ) );
+ }
+ }
+
+ public function get_services_total( $post_id = false ) {
+ $totals = array();
+ $services = $this->get_blog_services();
+
+ if ( ! empty( $services ) && isset( $services['all'] ) ) {
+ foreach ( $services['all'] as $key => $value ) {
+ $totals[ $key ] = new Sharing_Service_Total( $key, $this->get_total( $key, $post_id ) );
+ }
+ }
+ usort( $totals, array( 'Sharing_Service_Total', 'cmp' ) );
+
+ return $totals;
+ }
+
+ public function get_posts_total() {
+ $totals = array();
+ global $wpdb, $blog_id;
+
+ $my_data = $wpdb->get_results( $wpdb->prepare( 'SELECT post_id as id, SUM( count ) as total FROM sharing_stats WHERE blog_id = %d GROUP BY post_id ORDER BY count DESC ', $blog_id ) );
+
+ if ( ! empty( $my_data ) ) {
+ foreach ( $my_data as $row ) {
+ $totals[] = new Sharing_Post_Total( $row->id, $row->total );
+ }
+ }
+
+ usort( $totals, array( 'Sharing_Post_Total', 'cmp' ) );
+
+ return $totals;
+ }
+}
+
+class Sharing_Service_Total {
+ public $id = '';
+ public $name = '';
+ public $service = '';
+ public $total = 0;
+
+ public function __construct( $id, $total ) {
+ $services = new Sharing_Service();
+ $this->id = esc_html( $id );
+ $this->service = $services->get_service( $id );
+ $this->total = (int) $total;
+
+ $this->name = $this->service->get_name();
+ }
+
+ static function cmp( $a, $b ) {
+ if ( $a->total == $b->total ) {
+ return $a->name < $b->name;
+ }
+ return $a->total < $b->total;
+ }
+}
+
+class Sharing_Post_Total {
+ public $id = 0;
+ public $total = 0;
+ public $title = '';
+ public $url = '';
+
+ public function __construct( $id, $total ) {
+ $this->id = (int) $id;
+ $this->total = (int) $total;
+ $this->title = get_the_title( $this->id );
+ $this->url = get_permalink( $this->id );
+ }
+
+ static function cmp( $a, $b ) {
+ if ( $a->total == $b->total ) {
+ return $a->id < $b->id;
+ }
+ return $a->total < $b->total;
+ }
+}
+
+function sharing_register_post_for_share_counts( $post_id ) {
+ global $jetpack_sharing_counts;
+
+ if ( ! isset( $jetpack_sharing_counts ) || ! is_array( $jetpack_sharing_counts ) ) {
+ $jetpack_sharing_counts = array();
+ }
+
+ $jetpack_sharing_counts[ (int) $post_id ] = get_permalink( $post_id );
+}
+
+function sharing_maybe_enqueue_scripts() {
+ $sharer = new Sharing_Service();
+ $global_options = $sharer->get_global_options();
+
+ $enqueue = false;
+ if ( is_singular() && in_array( get_post_type(), $global_options['show'] ) ) {
+ $enqueue = true;
+ } elseif ( in_array( 'index', $global_options['show'] ) && ( is_home() || is_front_page() || is_archive() || is_search() || in_array( get_post_type(), $global_options['show'] ) ) ) {
+ $enqueue = true;
+ }
+
+ /**
+ * Filter to decide when sharing scripts should be enqueued.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.2.0
+ *
+ * @param bool $enqueue Decide if the sharing scripts should be enqueued.
+ */
+ return (bool) apply_filters( 'sharing_enqueue_scripts', $enqueue );
+}
+
+function sharing_add_footer() {
+ if (
+ class_exists( 'Jetpack_AMP_Support' )
+ && Jetpack_AMP_Support::is_amp_request()
+ ) {
+ return;
+ }
+
+ global $jetpack_sharing_counts;
+
+ /**
+ * Filter all JavaScript output by the sharing module.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param bool true Control whether the sharing module should add any JavaScript to the site. Default to true.
+ */
+ if ( apply_filters( 'sharing_js', true ) && sharing_maybe_enqueue_scripts() ) {
+
+ /**
+ * Filter the display of sharing counts next to the sharing buttons.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.2.0
+ *
+ * @param bool true Control the display of counters next to the sharing buttons. Default to true.
+ */
+ if ( apply_filters( 'jetpack_sharing_counts', true ) && is_array( $jetpack_sharing_counts ) && count( $jetpack_sharing_counts ) ) :
+ $sharing_post_urls = array_filter( $jetpack_sharing_counts );
+ if ( $sharing_post_urls ) :
+ ?>
+
+ <script type="text/javascript">
+ window.WPCOM_sharing_counts = <?php echo json_encode( array_flip( $sharing_post_urls ) ); ?>;
+ </script>
+ <?php
+ endif;
+ endif;
+
+ wp_enqueue_script( 'sharing-js' );
+ $sharing_js_options = array(
+ 'lang' => get_base_recaptcha_lang_code(),
+ /** This filter is documented in modules/sharedaddy/sharing-service.php */
+ 'counts' => apply_filters( 'jetpack_sharing_counts', true ),
+ 'is_stats_active' => Jetpack::is_module_active( 'stats' ),
+ );
+ wp_localize_script( 'sharing-js', 'sharing_js_options', $sharing_js_options );
+ }
+ $sharer = new Sharing_Service();
+ $enabled = $sharer->get_blog_services();
+ foreach ( array_merge( $enabled['visible'], $enabled['hidden'] ) as $service ) {
+ $service->display_footer();
+ }
+}
+
+function sharing_add_header() {
+ $sharer = new Sharing_Service();
+ $enabled = $sharer->get_blog_services();
+
+ foreach ( array_merge( $enabled['visible'], $enabled['hidden'] ) as $service ) {
+ $service->display_header();
+ }
+
+ if ( count( $enabled['all'] ) > 0 && sharing_maybe_enqueue_scripts() ) {
+ wp_enqueue_style( 'sharedaddy', plugin_dir_url( __FILE__ ) . 'sharing.css', array(), JETPACK__VERSION );
+ wp_enqueue_style( 'social-logos' );
+ }
+
+}
+add_action( 'wp_head', 'sharing_add_header', 1 );
+
+function sharing_process_requests() {
+ global $post;
+
+ // Only process if: single post and share=X defined
+ if ( ( is_page() || is_single() ) && isset( $_GET['share'] ) ) {
+ $sharer = new Sharing_Service();
+
+ $service = $sharer->get_service( $_GET['share'] );
+ if ( $service ) {
+ $service->process_request( $post, $_POST );
+ }
+ }
+}
+add_action( 'template_redirect', 'sharing_process_requests', 9 );
+
+function sharing_display( $text = '', $echo = false ) {
+ global $post, $wp_current_filter;
+
+ require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
+ if ( Jetpack_Sync_Settings::is_syncing() ) {
+ return $text;
+ }
+
+ if ( empty( $post ) ) {
+ return $text;
+ }
+
+ if ( ( is_preview() || is_admin() ) && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
+ return $text;
+ }
+
+ // Don't output flair on excerpts
+ if ( in_array( 'get_the_excerpt', (array) $wp_current_filter ) ) {
+ return $text;
+ }
+
+ // Don't allow flair to be added to the_content more than once (prevent infinite loops)
+ $done = false;
+ foreach ( $wp_current_filter as $filter ) {
+ if ( 'the_content' == $filter ) {
+ if ( $done ) {
+ return $text;
+ } else {
+ $done = true;
+ }
+ }
+ }
+
+ // check whether we are viewing the front page and whether the front page option is checked
+ $options = get_option( 'sharing-options' );
+ $display_options = $options['global']['show'];
+
+ if ( is_front_page() && ( is_array( $display_options ) && ! in_array( 'index', $display_options ) ) ) {
+ return $text;
+ }
+
+ if ( is_attachment() && in_array( 'the_excerpt', (array) $wp_current_filter ) ) {
+ // Many themes run the_excerpt() conditionally on an attachment page, then run the_content().
+ // We only want to output the sharing buttons once. Let's stick with the_content().
+ return $text;
+ }
+
+ $sharer = new Sharing_Service();
+ $global = $sharer->get_global_options();
+
+ $show = false;
+ if ( ! is_feed() ) {
+ if ( is_singular() && in_array( get_post_type(), $global['show'] ) ) {
+ $show = true;
+ } elseif ( in_array( 'index', $global['show'] ) && ( is_home() || is_front_page() || is_archive() || is_search() || in_array( get_post_type(), $global['show'] ) ) ) {
+ $show = true;
+ }
+ }
+
+ /**
+ * Filter to decide if sharing buttons should be displayed.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param bool $show Should the sharing buttons be displayed.
+ * @param WP_Post $post The post to share.
+ */
+ $show = apply_filters( 'sharing_show', $show, $post );
+
+ // Disabled for this post?
+ $switched_status = get_post_meta( $post->ID, 'sharing_disabled', false );
+
+ if ( ! empty( $switched_status ) ) {
+ $show = false;
+ }
+
+ // Private post?
+ $post_status = get_post_status( $post->ID );
+
+ if ( 'private' === $post_status ) {
+ $show = false;
+ }
+
+ /**
+ * Filter the Sharing buttons' Ajax action name Jetpack checks for.
+ * This allows the use of the buttons with your own Ajax implementation.
+ *
+ * @module sharedaddy
+ *
+ * @since 7.3.0
+ *
+ * @param string $sharing_ajax_action_name Name of the Sharing buttons' Ajax action.
+ */
+ $ajax_action = apply_filters( 'sharing_ajax_action', 'get_latest_posts' );
+
+ // Allow to be used in ajax requests for latest posts.
+ if (
+ defined( 'DOING_AJAX' )
+ && DOING_AJAX
+ && isset( $_REQUEST['action'] )
+ && $ajax_action === $_REQUEST['action']
+ ) {
+ $show = true;
+ }
+
+ $sharing_content = '';
+ $enabled = false;
+
+ if ( $show ) {
+ /**
+ * Filters the list of enabled Sharing Services.
+ *
+ * @module sharedaddy
+ *
+ * @since 2.2.3
+ *
+ * @param array $sharer->get_blog_services() Array of Sharing Services currently enabled.
+ */
+ $enabled = apply_filters( 'sharing_enabled', $sharer->get_blog_services() );
+
+ if ( count( $enabled['all'] ) > 0 ) {
+ global $post;
+
+ $dir = get_option( 'text_direction' );
+
+ // Wrapper
+ $sharing_content .= '<div class="sharedaddy sd-sharing-enabled"><div class="robots-nocontent sd-block sd-social sd-social-' . $global['button_style'] . ' sd-sharing">';
+ if ( $global['sharing_label'] != '' ) {
+ $sharing_content .= sprintf(
+ /**
+ * Filter the sharing buttons' headline structure.
+ *
+ * @module sharedaddy
+ *
+ * @since 4.4.0
+ *
+ * @param string $sharing_headline Sharing headline structure.
+ * @param string $global['sharing_label'] Sharing title.
+ * @param string $sharing Module name.
+ */
+ apply_filters( 'jetpack_sharing_headline_html', '<h3 class="sd-title">%s</h3>', $global['sharing_label'], 'sharing' ),
+ esc_html( $global['sharing_label'] )
+ );
+ }
+ $sharing_content .= '<div class="sd-content"><ul>';
+
+ // Visible items
+ $visible = '';
+ foreach ( $enabled['visible'] as $id => $service ) {
+ $klasses = array( 'share-' . $service->get_class() );
+ if ( $service->is_deprecated() ) {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ continue;
+ }
+ $klasses[] = 'share-deprecated';
+ }
+ // Individual HTML for sharing service
+ $visible .= '<li class="' . implode( ' ', $klasses ) . '">' . $service->get_display( $post ) . '</li>';
+ }
+
+ $parts = array();
+ $parts[] = $visible;
+ if ( count( $enabled['hidden'] ) > 0 ) {
+ if ( count( $enabled['visible'] ) > 0 ) {
+ $expand = __( 'More', 'jetpack' );
+ } else {
+ $expand = __( 'Share', 'jetpack' );
+ }
+ $parts[] = '<li><a href="#" class="sharing-anchor sd-button share-more"><span>' . $expand . '</span></a></li>';
+ }
+
+ if ( $dir == 'rtl' ) {
+ $parts = array_reverse( $parts );
+ }
+
+ $sharing_content .= implode( '', $parts );
+ $sharing_content .= '<li class="share-end"></li></ul>';
+
+ if ( count( $enabled['hidden'] ) > 0 ) {
+ $sharing_content .= '<div class="sharing-hidden"><div class="inner" style="display: none;';
+
+ if ( count( $enabled['hidden'] ) == 1 ) {
+ $sharing_content .= 'width:150px;';
+ }
+
+ $sharing_content .= '">';
+
+ if ( count( $enabled['hidden'] ) == 1 ) {
+ $sharing_content .= '<ul style="background-image:none;">';
+ } else {
+ $sharing_content .= '<ul>';
+ }
+
+ $count = 1;
+ foreach ( $enabled['hidden'] as $id => $service ) {
+ // Individual HTML for sharing service
+ $klasses = array( 'share-' . $service->get_class() );
+ if ( $service->is_deprecated() ) {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ continue;
+ }
+ $klasses[] = 'share-deprecated';
+ }
+ $sharing_content .= '<li class="' . implode( ' ', $klasses ) . '">';
+ $sharing_content .= $service->get_display( $post );
+ $sharing_content .= '</li>';
+
+ if ( ( $count % 2 ) == 0 ) {
+ $sharing_content .= '<li class="share-end"></li>';
+ }
+
+ $count ++;
+ }
+
+ // End of wrapper
+ $sharing_content .= '<li class="share-end"></li></ul></div></div>';
+ }
+
+ $sharing_content .= '</div></div></div>';
+
+ // Register our JS
+ if ( defined( 'JETPACK__VERSION' ) ) {
+ $ver = JETPACK__VERSION;
+ } else {
+ $ver = '20141212';
+ }
+ wp_register_script(
+ 'sharing-js',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/sharedaddy/sharing.min.js',
+ 'modules/sharedaddy/sharing.js'
+ ),
+ array( 'jquery' ),
+ $ver
+ );
+
+ // Enqueue scripts for the footer
+ add_action( 'wp_footer', 'sharing_add_footer' );
+ }
+ }
+
+ /**
+ * Filters the content markup of the Jetpack sharing links
+ *
+ * @module sharedaddy
+ *
+ * @since 3.8.0
+ * @since 6.2.0 Started sending $enabled as a second parameter.
+ *
+ * @param string $sharing_content Content markup of the Jetpack sharing links
+ * @param array $enabled Array of Sharing Services currently enabled.
+ */
+ $sharing_markup = apply_filters( 'jetpack_sharing_display_markup', $sharing_content, $enabled );
+
+ if ( $echo ) {
+ echo $text . $sharing_markup;
+ } else {
+ return $text . $sharing_markup;
+ }
+}
+
+add_filter( 'the_content', 'sharing_display', 19 );
+add_filter( 'the_excerpt', 'sharing_display', 19 );
+function get_base_recaptcha_lang_code() {
+
+ $base_recaptcha_lang_code_mapping = array(
+ 'en' => 'en',
+ 'nl' => 'nl',
+ 'fr' => 'fr',
+ 'fr-be' => 'fr',
+ 'fr-ca' => 'fr',
+ 'fr-ch' => 'fr',
+ 'de' => 'de',
+ 'pt' => 'pt',
+ 'pt-br' => 'pt',
+ 'ru' => 'ru',
+ 'es' => 'es',
+ 'tr' => 'tr',
+ );
+
+ $blog_lang_code = function_exists( 'get_blog_lang_code' ) ? get_blog_lang_code() : get_bloginfo( 'language' );
+ if ( isset( $base_recaptcha_lang_code_mapping[ $blog_lang_code ] ) ) {
+ return $base_recaptcha_lang_code_mapping[ $blog_lang_code ];
+ }
+
+ // if no base mapping is found return default 'en'
+ return 'en';
+}
diff --git a/plugins/jetpack/modules/sharedaddy/sharing-sources.php b/plugins/jetpack/modules/sharedaddy/sharing-sources.php
new file mode 100644
index 00000000..0cc0ec59
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/sharing-sources.php
@@ -0,0 +1,1793 @@
+<?php
+
+abstract class Sharing_Source {
+ public $button_style;
+ public $smart;
+ protected $open_link_in_new;
+ protected $id;
+
+ public function __construct( $id, array $settings ) {
+ $this->id = $id;
+ /**
+ * Filter the way sharing links open.
+ *
+ * By default, sharing links open in a new window.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.4.0
+ *
+ * @param bool true Should Sharing links open in a new window. Default to true.
+ */
+ $this->open_link_in_new = apply_filters( 'jetpack_open_sharing_in_new_window', true );
+
+ if ( isset( $settings['button_style'] ) ) {
+ $this->button_style = $settings['button_style'];
+ }
+
+ if ( isset( $settings['smart'] ) ) {
+ $this->smart = $settings['smart'];
+ }
+ }
+
+ public function is_deprecated() {
+ return false;
+ }
+
+ public function http() {
+ return is_ssl() ? 'https' : 'http';
+ }
+
+ public function get_id() {
+ return $this->id;
+ }
+
+ public function get_class() {
+ return $this->id;
+ }
+
+ public function get_share_url( $post_id ) {
+ /**
+ * Filter the sharing permalink.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.2.0
+ *
+ * @param string get_permalink( $post_id ) Post Permalink.
+ * @param int $post_id Post ID.
+ * @param int $this->id Sharing ID.
+ */
+ return apply_filters( 'sharing_permalink', get_permalink( $post_id ), $post_id, $this->id );
+ }
+
+ public function get_share_title( $post_id ) {
+ $post = get_post( $post_id );
+ /**
+ * Filter the sharing title.
+ *
+ * @module sharedaddy
+ *
+ * @since 2.8.0
+ *
+ * @param string $post->post_title Post Title.
+ * @param int $post_id Post ID.
+ * @param int $this->id Sharing ID.
+ */
+ $title = apply_filters( 'sharing_title', $post->post_title, $post_id, $this->id );
+
+ return html_entity_decode( wp_kses( $title, null ) );
+ }
+
+ public function has_custom_button_style() {
+ return false;
+ }
+
+ public function get_link( $url, $text, $title, $query = '', $id = false ) {
+ $args = func_get_args();
+ $klasses = array( 'share-' . $this->get_class(), 'sd-button' );
+
+ if ( 'icon' == $this->button_style || 'icon-text' == $this->button_style ) {
+ $klasses[] = 'share-icon';
+ }
+
+ if ( 'icon' == $this->button_style ) {
+ $text = $title;
+ $klasses[] = 'no-text';
+
+ if ( true == $this->open_link_in_new ) {
+ $text .= __( ' (Opens in new window)', 'jetpack' );
+ }
+ }
+
+ /**
+ * Filter the sharing display ID.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.4.0
+ *
+ * @param int|false $id Sharing ID.
+ * @param object $this Sharing service properties.
+ * @param array $args Array of sharing service options.
+ */
+ $id = apply_filters( 'jetpack_sharing_display_id', $id, $this, $args );
+ /**
+ * Filter the sharing display link.
+ *
+ * @module sharedaddy
+ *
+ * @since 2.8.0
+ *
+ * @param string $url Post URL.
+ * @param object $this Sharing service properties.
+ * @param int|false $id Sharing ID.
+ * @param array $args Array of sharing service options.
+ */
+ $url = apply_filters( 'sharing_display_link', $url, $this, $id, $args ); // backwards compatibility
+ /**
+ * Filter the sharing display link.
+ *
+ * @module sharedaddy
+ *
+ * @since 2.8.0
+ *
+ * @param string $url Post URL.
+ * @param object $this Sharing service properties.
+ * @param int|false $id Sharing ID.
+ * @param array $args Array of sharing service options.
+ */
+ $url = apply_filters( 'jetpack_sharing_display_link', $url, $this, $id, $args );
+ /**
+ * Filter the sharing display query.
+ *
+ * @module sharedaddy
+ *
+ * @since 2.8.0
+ *
+ * @param string $query Sharing service URL parameter.
+ * @param object $this Sharing service properties.
+ * @param int|false $id Sharing ID.
+ * @param array $args Array of sharing service options.
+ */
+ $query = apply_filters( 'jetpack_sharing_display_query', $query, $this, $id, $args );
+
+ if ( ! empty( $query ) ) {
+ if ( false === stripos( $url, '?' ) ) {
+ $url .= '?' . $query;
+ } else {
+ $url .= '&amp;' . $query;
+ }
+ }
+
+ if ( 'text' == $this->button_style ) {
+ $klasses[] = 'no-icon';
+ }
+
+ /**
+ * Filter the sharing display classes.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.4.0
+ *
+ * @param array $klasses Sharing service classes.
+ * @param object $this Sharing service properties.
+ * @param int|false $id Sharing ID.
+ * @param array $args Array of sharing service options.
+ */
+ $klasses = apply_filters( 'jetpack_sharing_display_classes', $klasses, $this, $id, $args );
+ /**
+ * Filter the sharing display title.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.4.0
+ *
+ * @param string $title Sharing service title.
+ * @param object $this Sharing service properties.
+ * @param int|false $id Sharing ID.
+ * @param array $args Array of sharing service options.
+ */
+ $title = apply_filters( 'jetpack_sharing_display_title', $title, $this, $id, $args );
+ /**
+ * Filter the sharing display text.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.4.0
+ *
+ * @param string $text Sharing service text.
+ * @param object $this Sharing service properties.
+ * @param int|false $id Sharing ID.
+ * @param array $args Array of sharing service options.
+ */
+ $text = apply_filters( 'jetpack_sharing_display_text', $text, $this, $id, $args );
+
+ return sprintf(
+ '<a rel="nofollow%s" data-shared="%s" class="%s" href="%s"%s title="%s"><span%s>%s</span></a>',
+ ( true == $this->open_link_in_new ) ? ' noopener noreferrer' : '',
+ ( $id ? esc_attr( $id ) : '' ),
+ implode( ' ', $klasses ),
+ $url,
+ ( true == $this->open_link_in_new ) ? ' target="_blank"' : '',
+ $title,
+ ( 'icon' == $this->button_style ) ? '></span><span class="sharing-screen-reader-text"' : '',
+ $text
+ );
+ }
+
+ /**
+ * Get an unfiltered post permalink to use when generating a sharing URL with get_link.
+ * Use instead of get_share_url for non-official styles as get_permalink ensures that process_request
+ * will be executed more reliably, in the case that the filtered URL uses a service that strips query parameters.
+ *
+ * @since 3.7.0
+ * @param int $post_id Post ID.
+ * @uses get_permalink
+ * @return string get_permalink( $post_id ) Post permalink.
+ */
+ public function get_process_request_url( $post_id ) {
+ return get_permalink( $post_id );
+ }
+
+ abstract public function get_name();
+ abstract public function get_display( $post );
+
+ public function display_header() {
+ }
+
+ public function display_footer() {
+ }
+
+ public function has_advanced_options() {
+ return false;
+ }
+
+ public function display_preview( $echo = true, $force_smart = false, $button_style = null ) {
+ $text = '&nbsp;';
+ $button_style = ( ! empty( $button_style ) ) ? $button_style : $this->button_style;
+ if ( ! $this->smart && ! $force_smart ) {
+ if ( $button_style != 'icon' ) {
+ $text = $this->get_name();
+ }
+ }
+
+ $klasses = array( 'share-' . $this->get_class(), 'sd-button' );
+
+ if ( $button_style == 'icon' || $button_style == 'icon-text' ) {
+ $klasses[] = 'share-icon';
+ }
+
+ if ( $button_style == 'icon' ) {
+ $klasses[] = 'no-text';
+ }
+
+ if ( $button_style == 'text' ) {
+ $klasses[] = 'no-icon';
+ }
+
+ $is_deprecated = $this->is_deprecated();
+
+ $link = sprintf(
+ '<a rel="nofollow" class="%s" href="javascript:void(0)" title="%s"><span>%s</span></a>',
+ implode( ' ', $klasses ),
+ esc_attr(
+ $is_deprecated
+ ? sprintf( __( 'The %1$s service has shut down. This sharing button is not displayed to your visitors and should be removed.', 'jetpack' ), $this->get_name() )
+ : $this->get_name()
+ ),
+ esc_html(
+ $is_deprecated
+ ? sprintf( __( '%1$s has shut down', 'jetpack' ), $this->get_name() )
+ : $text
+ )
+ );
+
+ $smart = ( $this->smart || $force_smart ) ? 'on' : 'off';
+ $return = "<div class='option option-smart-$smart'>$link</div>";
+ if ( $echo ) {
+ echo $return;
+ }
+
+ return $return;
+ }
+
+ public function get_total( $post = false ) {
+ global $wpdb, $blog_id;
+
+ $name = strtolower( $this->get_id() );
+
+ if ( $post == false ) {
+ // get total number of shares for service
+ return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d AND share_service = %s', $blog_id, $name ) );
+ }
+
+ // get total shares for a post
+ return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT count FROM sharing_stats WHERE blog_id = %d AND post_id = %d AND share_service = %s', $blog_id, $post->ID, $name ) );
+ }
+
+ public function get_posts_total() {
+ global $wpdb, $blog_id;
+
+ $totals = array();
+ $name = strtolower( $this->get_id() );
+
+ $my_data = $wpdb->get_results( $wpdb->prepare( 'SELECT post_id as id, SUM( count ) as total FROM sharing_stats WHERE blog_id = %d AND share_service = %s GROUP BY post_id ORDER BY count DESC ', $blog_id, $name ) );
+
+ if ( ! empty( $my_data ) ) {
+ foreach ( $my_data as $row ) {
+ $totals[] = new Sharing_Post_Total( $row->id, $row->total );
+ }
+ }
+
+ usort( $totals, array( 'Sharing_Post_Total', 'cmp' ) );
+
+ return $totals;
+ }
+
+ public function process_request( $post, array $post_data ) {
+ /**
+ * Fires when a post is shared via one of the sharing buttons.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param array $args Aray of information about the sharing service.
+ */
+ do_action( 'sharing_bump_stats', array( 'service' => $this, 'post' => $post ) );
+ }
+
+ public function js_dialog( $name, $params = array() ) {
+ if ( true !== $this->open_link_in_new ) {
+ return;
+ }
+
+ $defaults = array(
+ 'menubar' => 1,
+ 'resizable' => 1,
+ 'width' => 600,
+ 'height' => 400,
+ );
+ $params = array_merge( $defaults, $params );
+ $opts = array();
+ foreach ( $params as $key => $val ) {
+ $opts[] = "$key=$val";
+ }
+ $opts = implode( ',', $opts );
+
+ // Add JS after sharing-js has been enqueued.
+ wp_add_inline_script( 'sharing-js',
+ "var windowOpen;
+ jQuery( document.body ).on( 'click', 'a.share-$name', function() {
+ // If there's another sharing window open, close it.
+ if ( 'undefined' !== typeof windowOpen ) {
+ windowOpen.close();
+ }
+ windowOpen = window.open( jQuery( this ).attr( 'href' ), 'wpcom$name', '$opts' );
+ return false;
+ });"
+ );
+ }
+}
+
+abstract class Deprecated_Sharing_Source extends Sharing_Source {
+ public $button_style = 'text';
+ public $smart = false;
+ protected $open_link_in_new = false;
+ protected $id;
+ protected $deprecated = true;
+
+ final public function __construct( $id, array $settings ) {
+ $this->id = $id;
+
+ if ( isset( $settings['button_style'] ) ) {
+ $this->button_style = $settings['button_style'];
+ }
+ }
+
+ final public function is_deprecated() {
+ return true;
+ }
+
+ final public function get_share_url( $post_id ) {
+ return get_permalink( $post_id );
+ }
+
+ final public function display_preview( $echo = true, $force_smart = false, $button_style = null ) {
+ return parent::display_preview( $echo, false, $button_style );
+ }
+
+ final public function get_total( $post = false ) {
+ return 0;
+ }
+
+ final public function get_posts_total() {
+ return 0;
+ }
+
+ final public function process_request( $post, array $post_data ) {
+ parent::process_request( $post, $post_data );
+ }
+
+ final public function get_display( $post ) {
+ if ( current_user_can( 'manage_options' ) ) {
+ return $this->display_deprecated( $post );
+ }
+
+ return '';
+ }
+
+ public function display_deprecated( $post ) {
+ return $this->get_link(
+ $this->get_share_url( $post->ID ),
+ sprintf( __( '%1$s has shut down', 'jetpack' ), $this->get_name() ),
+ sprintf( __( 'The %1$s service has shut down. This sharing button is not displayed to your visitors and should be removed.', 'jetpack' ), $this->get_name() )
+ );
+ }
+}
+
+abstract class Sharing_Advanced_Source extends Sharing_Source {
+ public function has_advanced_options() {
+ return true;
+ }
+
+ abstract public function display_options();
+ abstract public function update_options( array $data );
+ abstract public function get_options();
+}
+
+class Share_Email extends Sharing_Source {
+ public $shortname = 'email';
+ public $icon = '\f410';
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+ }
+
+ public function get_name() {
+ return _x( 'Email', 'as sharing source', 'jetpack' );
+ }
+
+ // Default does nothing
+ public function process_request( $post, array $post_data ) {
+ $ajax = false;
+ if ( isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) == 'xmlhttprequest' ) {
+ $ajax = true;
+ }
+
+ $source_email = $target_email = $source_name = false;
+
+ if ( isset( $post_data['source_email'] ) && is_email( $post_data['source_email'] ) ) {
+ $source_email = $post_data['source_email'];
+ }
+
+ if ( isset( $post_data['target_email'] ) && is_email( $post_data['target_email'] ) ) {
+ $target_email = $post_data['target_email'];
+ }
+
+ if ( isset( $post_data['source_name'] ) && strlen( $post_data['source_name'] ) < 200 ) {
+ $source_name = $post_data['source_name'];
+ } elseif ( isset( $post_data['source_name'] ) ) {
+ $source_name = substr( $post_data['source_name'], 0, 200 );
+ } else {
+ $source_name = '';
+ }
+
+ // Test email
+ $error = 1; // Failure in data
+ if ( empty( $post_data['source_f_name'] ) && $source_email && $target_email && $source_name ) {
+ /**
+ * Allow plugins to stop the email sharing button from running the shared message through Akismet.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param bool true Should we check if the message isn't spam?
+ * @param object $post Post information.
+ * @param array $post_data Information about the shared message.
+ */
+ if ( apply_filters( 'sharing_email_check', true, $post, $post_data ) ) {
+ $data = array(
+ 'post' => $post,
+ 'source' => $source_email,
+ 'target' => $target_email,
+ 'name' => $source_name,
+ 'sharing_source' => $this,
+ );
+ // todo: implement an error message when email doesn't get sent.
+ /**
+ * Filter whether an email can be sent from the Email sharing button.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param array $data Array of information about the shared message.
+ */
+ if ( ( $data = apply_filters( 'sharing_email_can_send', $data ) ) !== false ) {
+ // Record stats
+ parent::process_request( $data['post'], $post_data );
+
+ /**
+ * Fires when an email is sent via the Email sharing button.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param array $data Array of information about the shared message.
+ */
+ do_action( 'sharing_email_send_post', $data );
+ }
+
+ // Return a positive regardless of whether the user is subscribed or not
+ if ( $ajax ) {
+?>
+<div class="response">
+ <div class="response-title"><?php _e( 'This post has been shared!', 'jetpack' ); ?></div>
+ <div class="response-sub"><?php printf( __( 'You have shared this post with %s', 'jetpack' ), esc_html( $target_email ) ); ?></div>
+ <div class="response-close"><a href="#" class="sharing_cancel"><?php _e( 'Close', 'jetpack' ); ?></a></div>
+</div>
+<?php
+ } else {
+ wp_safe_redirect( get_permalink( $post->ID ) . '?shared=email' );
+ }
+
+ die();
+ } else {
+ $error = 2; // Email check failed
+ }
+ }
+
+ if ( $ajax ) {
+ echo $error;
+ } else {
+ wp_safe_redirect( get_permalink( $post->ID ) . '?shared=email&msg=fail' );
+ }
+
+ die();
+ }
+
+ public function get_display( $post ) {
+ return $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'Email', 'share to', 'jetpack' ), __( 'Click to email this to a friend', 'jetpack' ), 'share=email' );
+ }
+
+ /**
+ * Outputs the hidden email dialog
+ */
+ public function display_footer() {
+ global $current_user;
+
+ $visible = $status = false;
+?>
+ <div id="sharing_email" style="display: none;">
+ <form action="<?php echo esc_url( $_SERVER['REQUEST_URI'] ); ?>" method="post">
+ <label for="target_email"><?php _e( 'Send to Email Address', 'jetpack' ) ?></label>
+ <input type="email" name="target_email" id="target_email" value="" />
+
+ <?php if ( is_user_logged_in() ) : ?>
+ <input type="hidden" name="source_name" value="<?php echo esc_attr( $current_user->display_name ); ?>" />
+ <input type="hidden" name="source_email" value="<?php echo esc_attr( $current_user->user_email ); ?>" />
+ <?php else : ?>
+
+ <label for="source_name"><?php _e( 'Your Name', 'jetpack' ) ?></label>
+ <input type="text" name="source_name" id="source_name" value="" />
+
+ <label for="source_email"><?php _e( 'Your Email Address', 'jetpack' ) ?></label>
+ <input type="email" name="source_email" id="source_email" value="" />
+
+ <?php endif; ?>
+ <input type="text" id="jetpack-source_f_name" name="source_f_name" class="input" value="" size="25" autocomplete="off" title="<?php esc_attr_e( 'This field is for validation and should not be changed', 'jetpack' ); ?>" />
+ <?php
+ /**
+ * Fires when the Email sharing dialog is loaded.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ *
+ * @param string jetpack Eail sharing source.
+ */
+ do_action( 'sharing_email_dialog', 'jetpack' );
+ ?>
+
+ <img style="float: right; display: none" class="loading" src="<?php
+ /** This filter is documented in modules/stats.php */
+ echo apply_filters( 'jetpack_static_url', plugin_dir_url( __FILE__ ) . 'images/loading.gif' ); ?>" alt="loading" width="16" height="16" />
+ <input type="submit" value="<?php esc_attr_e( 'Send Email', 'jetpack' ); ?>" class="sharing_send" />
+ <a rel="nofollow" href="#cancel" class="sharing_cancel" role="button"><?php _e( 'Cancel', 'jetpack' ); ?></a>
+
+ <div class="errors errors-1" style="display: none;">
+ <?php _e( 'Post was not sent - check your email addresses!', 'jetpack' ); ?>
+ </div>
+
+ <div class="errors errors-2" style="display: none;">
+ <?php _e( 'Email check failed, please try again', 'jetpack' ); ?>
+ </div>
+
+ <div class="errors errors-3" style="display: none;">
+ <?php _e( 'Sorry, your blog cannot share posts by email.', 'jetpack' ); ?>
+ </div>
+ </form>
+ </div>
+<?php
+ }
+}
+
+class Share_Twitter extends Sharing_Source {
+ public $shortname = 'twitter';
+ public $icon = '\f202';
+ // 'https://dev.twitter.com/rest/reference/get/help/configuration' ( 2015/02/06 ) short_url_length is 22, short_url_length_https is 23
+ public $short_url_length = 24;
+
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+ }
+
+ public function get_name() {
+ return __( 'Twitter', 'jetpack' );
+ }
+
+ /**
+ * Determine the Twitter 'via' value for a post.
+ *
+ * @param WP_Post|int $post Post object or post ID.
+ * @return string Twitter handle without the preceding @.
+ **/
+ public static function sharing_twitter_via( $post ) {
+ $post = get_post( $post );
+ /**
+ * Allow third-party plugins to customize the Twitter username used as "twitter:site" Twitter Card Meta Tag.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.0.0
+ *
+ * @param string $string Twitter Username.
+ * @param array $args Array of Open Graph Meta Tags and Twitter Cards tags.
+ */
+ $twitter_site_tag_value = apply_filters(
+ 'jetpack_twitter_cards_site_tag',
+ '',
+ /** This action is documented in modules/sharedaddy/sharing-sources.php */
+ array( 'twitter:creator' => apply_filters( 'jetpack_sharing_twitter_via', '', $post->ID ) )
+ );
+
+ /*
+ * Hack to remove the unwanted behavior of adding 'via @jetpack' which
+ * was introduced with the adding of the Twitter cards.
+ * This should be a temporary solution until a better method is setup.
+ */
+ if ( 'jetpack' == $twitter_site_tag_value ) {
+ $twitter_site_tag_value = '';
+ }
+
+ /**
+ * Filters the Twitter username used as "via" in the Twitter sharing button.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.7.0
+ *
+ * @param string $twitter_site_tag_value Twitter Username.
+ * @param int $post->ID Post ID.
+ */
+ $twitter_site_tag_value = apply_filters( 'jetpack_sharing_twitter_via', $twitter_site_tag_value, $post->ID );
+
+ // Strip out anything other than a letter, number, or underscore.
+ // This will prevent the inadvertent inclusion of an extra @, as well as normalizing the handle.
+ return preg_replace( '/[^\da-z_]+/i', '', $twitter_site_tag_value );
+ }
+
+ /**
+ * Determine the 'related' Twitter accounts for a post.
+ *
+ * @param WP_Post|int $post Post object or post ID.
+ * @return string Comma-separated list of Twitter handles.
+ **/
+ public static function get_related_accounts( $post ) {
+ $post = get_post( $post );
+ /**
+ * Filter the list of related Twitter accounts added to the Twitter sharing button.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.7.0
+ *
+ * @param array $args Array of Twitter usernames. Format is 'username' => 'Optional description'
+ * @param int $post->ID Post ID.
+ */
+ $related_accounts = apply_filters( 'jetpack_sharing_twitter_related', array(), $post->ID );
+
+ // Example related string: account1,account2:Account 2 description,account3
+ $related = array();
+
+ foreach ( $related_accounts as $related_account_username => $related_account_description ) {
+ // Join the description onto the end of the username
+ if ( $related_account_description ) {
+ $related_account_username .= ':' . $related_account_description;
+ }
+
+ $related[] = $related_account_username;
+ }
+
+ return implode( ',', $related );
+ }
+
+ public function get_display( $post ) {
+ $via = $this->sharing_twitter_via( $post );
+
+ if ( $via ) {
+ $via = 'data-via="' . esc_attr( $via ) . '"';
+ } else {
+ $via = '';
+ }
+
+ $related = $this->get_related_accounts( $post );
+ if ( ! empty( $related ) && $related !== $via ) {
+ $related = 'data-related="' . esc_attr( $related ) . '"';
+ } else {
+ $related = '';
+ }
+
+ if ( $this->smart ) {
+ $share_url = $this->get_share_url( $post->ID );
+ $post_title = $this->get_share_title( $post->ID );
+ return sprintf(
+ '<a href="https://twitter.com/share" class="twitter-share-button" data-url="%1$s" data-text="%2$s" %3$s %4$s>Tweet</a>',
+ esc_url( $share_url ),
+ esc_attr( $post_title ),
+ $via,
+ $related
+ );
+ } else {
+ if (
+ /**
+ * Allow plugins to disable sharing counts for specific sharing services.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.0.0
+ *
+ * @param bool true Should sharing counts be enabled for this specific service. Default to true.
+ * @param int $post->ID Post ID.
+ * @param string $str Sharing service name.
+ */
+ apply_filters( 'jetpack_register_post_for_share_counts', true, $post->ID, 'twitter' )
+ ) {
+ sharing_register_post_for_share_counts( $post->ID );
+ }
+ return $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'Twitter', 'share to', 'jetpack' ), __( 'Click to share on Twitter', 'jetpack' ), 'share=twitter', 'sharing-twitter-' . $post->ID );
+ }
+ }
+
+ public function process_request( $post, array $post_data ) {
+ $post_title = $this->get_share_title( $post->ID );
+ $post_link = $this->get_share_url( $post->ID );
+
+ if ( function_exists( 'mb_stripos' ) ) {
+ $strlen = 'mb_strlen';
+ $substr = 'mb_substr';
+ } else {
+ $strlen = 'strlen';
+ $substr = 'substr';
+ }
+
+ $via = $this->sharing_twitter_via( $post );
+ $related = $this->get_related_accounts( $post );
+ if ( $via ) {
+ $sig = " via @$via";
+ if ( $related === $via ) {
+ $related = false;
+ }
+ } else {
+ $via = false;
+ $sig = '';
+ }
+
+ $suffix_length = $this->short_url_length + $strlen( $sig );
+ // $sig is handled by twitter in their 'via' argument.
+ // $post_link is handled by twitter in their 'url' argument.
+ if ( 280 < $strlen( $post_title ) + $suffix_length ) {
+ // The -1 is for "\xE2\x80\xA6", a UTF-8 ellipsis.
+ $text = $substr( $post_title, 0, 280 - $suffix_length - 1 ) . "\xE2\x80\xA6";
+ } else {
+ $text = $post_title;
+ }
+
+ // Record stats
+ parent::process_request( $post, $post_data );
+
+ $url = $post_link;
+ $twitter_url = add_query_arg(
+ rawurlencode_deep( array_filter( compact( 'via', 'related', 'text', 'url' ) ) ),
+ 'https://twitter.com/intent/tweet'
+ );
+
+ // Redirect to Twitter
+ wp_redirect( $twitter_url );
+ die();
+ }
+
+ public function has_custom_button_style() {
+ return $this->smart;
+ }
+
+ public function display_footer() {
+ if ( $this->smart ) {
+ ?>
+ <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>
+ <?php
+ } else {
+ $this->js_dialog( $this->shortname, array( 'height' => 350 ) );
+ }
+ }
+}
+
+
+class Share_Reddit extends Sharing_Source {
+ public $shortname = 'reddit';
+ public $icon = '\f222';
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+ }
+
+ public function get_name() {
+ return __( 'Reddit', 'jetpack' );
+ }
+
+ public function get_display( $post ) {
+ if ( $this->smart ) {
+ return '<div class="reddit_button"><iframe src="' . $this->http() . '://www.reddit.com/static/button/button1.html?newwindow=true&width=120&amp;url=' . rawurlencode( $this->get_share_url( $post->ID ) ) . '&amp;title=' . rawurlencode( $this->get_share_title( $post->ID ) ) . '" height="22" width="120" scrolling="no" frameborder="0"></iframe></div>';
+ } else {
+ return $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'Reddit', 'share to', 'jetpack' ), __( 'Click to share on Reddit', 'jetpack' ), 'share=reddit' );
+ }
+ }
+
+ public function process_request( $post, array $post_data ) {
+ $reddit_url = $this->http() . '://reddit.com/submit?url=' . rawurlencode( $this->get_share_url( $post->ID ) ) . '&title=' . rawurlencode( $this->get_share_title( $post->ID ) );
+
+ // Record stats
+ parent::process_request( $post, $post_data );
+
+ // Redirect to Reddit
+ wp_redirect( $reddit_url );
+ die();
+ }
+}
+
+class Share_LinkedIn extends Sharing_Source {
+ public $shortname = 'linkedin';
+ public $icon = '\f207';
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+ }
+
+ public function get_name() {
+ return __( 'LinkedIn', 'jetpack' );
+ }
+
+ public function has_custom_button_style() {
+ return $this->smart;
+ }
+
+ public function get_display( $post ) {
+ $display = '';
+
+ if ( $this->smart ) {
+ $share_url = $this->get_share_url( $post->ID );
+ $display .= sprintf( '<div class="linkedin_button"><script type="in/share" data-url="%s" data-counter="right"></script></div>', esc_url( $share_url ) );
+ } else {
+ $display = $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'LinkedIn', 'share to', 'jetpack' ), __( 'Click to share on LinkedIn', 'jetpack' ), 'share=linkedin', 'sharing-linkedin-' . $post->ID );
+ }
+
+ /** This filter is already documented in modules/sharedaddy/sharing-sources.php */
+ if ( apply_filters( 'jetpack_register_post_for_share_counts', true, $post->ID, 'linkedin' ) ) {
+ sharing_register_post_for_share_counts( $post->ID );
+ }
+
+ return $display;
+ }
+
+ public function process_request( $post, array $post_data ) {
+
+ $post_link = $this->get_share_url( $post->ID );
+
+ // Using the same URL as the official button, which is *not* LinkedIn's documented sharing link
+ // https://www.linkedin.com/cws/share?url={url}&token=&isFramed=false
+ $linkedin_url = add_query_arg( array(
+ 'url' => rawurlencode( $post_link ),
+ ), 'https://www.linkedin.com/cws/share?token=&isFramed=false' );
+
+ // Record stats
+ parent::process_request( $post, $post_data );
+
+ // Redirect to LinkedIn
+ wp_redirect( $linkedin_url );
+ die();
+ }
+
+ public function display_footer() {
+ if ( ! $this->smart ) {
+ $this->js_dialog( $this->shortname, array( 'width' => 580, 'height' => 450 ) );
+ } else {
+ ?><script type="text/javascript">
+ jQuery( document ).ready( function() {
+ jQuery.getScript( 'https://platform.linkedin.com/in.js?async=true', function success() {
+ IN.init();
+ });
+ });
+ jQuery( document.body ).on( 'post-load', function() {
+ if ( typeof IN != 'undefined' )
+ IN.parse();
+ });
+ </script><?php
+ }
+ }
+}
+
+class Share_Facebook extends Sharing_Source {
+ public $shortname = 'facebook';
+ public $icon = '\f204';
+ private $share_type = 'default';
+
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+
+ if ( isset( $settings['share_type'] ) ) {
+ $this->share_type = $settings['share_type'];
+ }
+
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+ }
+
+ public function get_name() {
+ return __( 'Facebook', 'jetpack' );
+ }
+
+ public function display_header() {
+ }
+
+ function guess_locale_from_lang( $lang ) {
+ if ( 'en' == $lang || 'en_US' == $lang || ! $lang ) {
+ return 'en_US';
+ }
+
+ if ( ! class_exists( 'GP_Locales' ) ) {
+ if ( ! defined( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) || ! file_exists( JETPACK__GLOTPRESS_LOCALES_PATH ) ) {
+ return false;
+ }
+
+ require JETPACK__GLOTPRESS_LOCALES_PATH;
+ }
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ // WP.com: get_locale() returns 'it'
+ $locale = GP_Locales::by_slug( $lang );
+ } else {
+ // Jetpack: get_locale() returns 'it_IT';
+ $locale = GP_Locales::by_field( 'wp_locale', $lang );
+ }
+
+ if ( ! $locale ) {
+ return false;
+ }
+
+ if ( empty( $locale->facebook_locale ) ) {
+ if ( empty( $locale->wp_locale ) ) {
+ return false;
+ } else {
+ // Facebook SDK is smart enough to fall back to en_US if a
+ // locale isn't supported. Since supported Facebook locales
+ // can fall out of sync, we'll attempt to use the known
+ // wp_locale value and rely on said fallback.
+ return $locale->wp_locale;
+ }
+ }
+
+ return $locale->facebook_locale;
+ }
+
+ public function get_display( $post ) {
+ if ( $this->smart ) {
+ $share_url = $this->get_share_url( $post->ID );
+ $fb_share_html = '<div class="fb-share-button" data-href="' . esc_attr( $share_url ) . '" data-layout="button_count"></div>';
+ /**
+ * Filter the output of the Facebook Sharing button.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.6.0
+ *
+ * @param string $fb_share_html Facebook Sharing button HTML.
+ * @param string $share_url URL of the post to share.
+ */
+ return apply_filters( 'jetpack_sharing_facebook_official_button_output', $fb_share_html, $share_url );
+ }
+
+ /** This filter is already documented in modules/sharedaddy/sharing-sources.php */
+ if ( apply_filters( 'jetpack_register_post_for_share_counts', true, $post->ID, 'facebook' ) ) {
+ sharing_register_post_for_share_counts( $post->ID );
+ }
+ return $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'Facebook', 'share to', 'jetpack' ), __( 'Click to share on Facebook', 'jetpack' ), 'share=facebook', 'sharing-facebook-' . $post->ID );
+ }
+
+ public function process_request( $post, array $post_data ) {
+ $fb_url = $this->http() . '://www.facebook.com/sharer.php?u=' . rawurlencode( $this->get_share_url( $post->ID ) ) . '&t=' . rawurlencode( $this->get_share_title( $post->ID ) );
+
+ // Record stats
+ parent::process_request( $post, $post_data );
+
+ // Redirect to Facebook
+ wp_redirect( $fb_url );
+ die();
+ }
+
+ public function display_footer() {
+ $this->js_dialog( $this->shortname );
+ if ( $this->smart ) {
+ $locale = $this->guess_locale_from_lang( get_locale() );
+ if ( ! $locale ) {
+ $locale = 'en_US';
+ }
+ /**
+ * Filter the App ID used in the official Facebook Share button.
+ *
+ * @since 3.8.0
+ *
+ * @param int $fb_app_id Facebook App ID. Default to 249643311490 (WordPress.com's App ID).
+ */
+ $fb_app_id = apply_filters( 'jetpack_sharing_facebook_app_id', '249643311490' );
+ if ( is_numeric( $fb_app_id ) ) {
+ $fb_app_id = '&appId=' . $fb_app_id;
+ } else {
+ $fb_app_id = '';
+ }
+ ?><div id="fb-root"></div>
+ <script>(function(d, s, id) { var js, fjs = d.getElementsByTagName(s)[0]; if (d.getElementById(id)) return; js = d.createElement(s); js.id = id; js.src = 'https://connect.facebook.net/<?php echo $locale; ?>/sdk.js#xfbml=1<?php echo $fb_app_id; ?>&version=v2.3'; fjs.parentNode.insertBefore(js, fjs); }(document, 'script', 'facebook-jssdk'));</script>
+ <script>
+ jQuery( document.body ).on( 'post-load', function() {
+ if ( 'undefined' !== typeof FB ) {
+ FB.XFBML.parse();
+ }
+ } );
+ </script>
+ <?php
+ }
+ }
+}
+
+class Share_Print extends Sharing_Source {
+ public $shortname = 'print';
+ public $icon = '\f469';
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+ }
+
+ public function get_name() {
+ return __( 'Print', 'jetpack' );
+ }
+
+ public function get_display( $post ) {
+ return $this->get_link( $this->get_process_request_url( $post->ID ) . ( ( is_single() || is_page() ) ? '#print': '' ), _x( 'Print', 'share to', 'jetpack' ), __( 'Click to print', 'jetpack' ) );
+ }
+}
+
+class Share_PressThis extends Sharing_Source {
+ public $shortname = 'pressthis';
+ public $icon = '\f205';
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+ }
+
+ public function get_name() {
+ return __( 'Press This', 'jetpack' );
+ }
+
+ public function process_request( $post, array $post_data ) {
+ global $current_user;
+
+ $primary_blog = (int) get_user_meta( $current_user->ID, 'primary_blog', true );
+ if ( $primary_blog ) {
+ $primary_blog_details = get_blog_details( $primary_blog );
+ } else {
+ $primary_blog_details = false;
+ }
+
+ if ( $primary_blog_details ) {
+ $blogs = array( $primary_blog_details );
+ } elseif ( function_exists( 'get_active_blogs_for_user' ) ) {
+ $blogs = get_active_blogs_for_user();
+ if ( empty( $blogs ) ) {
+ $blogs = get_blogs_of_user( $current_user->ID );
+ }
+ } else {
+ $blogs = get_blogs_of_user( $current_user->ID );
+ }
+
+ if ( empty( $blogs ) ) {
+ wp_safe_redirect( get_permalink( $post->ID ) );
+ die();
+ }
+
+ $blog = current( $blogs );
+
+ $args = array(
+ 'u' => rawurlencode( $this->get_share_url( $post->ID ) ),
+ );
+
+ $args[ 'url-scan-submit' ] = 'Scan';
+ $args[ '_wpnonce' ] = wp_create_nonce( 'scan-site' );
+
+ $url = $blog->siteurl . '/wp-admin/press-this.php';
+ $url = add_query_arg( $args, $url );
+
+ // Record stats
+ parent::process_request( $post, $post_data );
+
+ // Redirect to Press This
+ wp_redirect( $url );
+ die();
+ }
+
+ public function get_display( $post ) {
+ return $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'Press This', 'share to', 'jetpack' ), __( 'Click to Press This!', 'jetpack' ), 'share=press-this' );
+ }
+}
+
+class Share_Custom extends Sharing_Advanced_Source {
+ private $name;
+ private $icon;
+ private $url;
+ public $smart = true;
+ public $shortname;
+
+ public function get_class() {
+ return 'custom share-custom-' . sanitize_html_class( strtolower( $this->name ) );
+ }
+
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+
+ $opts = $this->get_options();
+
+ if ( isset( $settings['name'] ) ) {
+ $this->name = $settings['name'];
+ $this->shortname = preg_replace( '/[^a-z0-9]*/', '', $settings['name'] );
+ }
+
+ if ( isset( $settings['icon'] ) ) {
+ $this->icon = $settings['icon'];
+
+ $new_icon = esc_url_raw( wp_specialchars_decode( $this->icon, ENT_QUOTES ) );
+ $i = 0;
+ while ( $new_icon != $this->icon ) {
+ if ( $i > 5 ) {
+ $this->icon = false;
+ break;
+ } else {
+ $this->icon = $new_icon;
+ $new_icon = esc_url_raw( wp_specialchars_decode( $this->icon, ENT_QUOTES ) );
+ }
+ $i++;
+ }
+ }
+
+ if ( isset( $settings['url'] ) ) {
+ $this->url = $settings['url'];
+ }
+ }
+
+ public function get_name() {
+ return $this->name;
+ }
+
+ public function get_display( $post ) {
+ $str = $this->get_link( $this->get_process_request_url( $post->ID ), esc_html( $this->name ), sprintf( __( 'Click to share on %s', 'jetpack' ), esc_attr( $this->name ) ), 'share=' . $this->id );
+ return str_replace( '<span>', '<span style="' . esc_attr( 'background-image:url("' . addcslashes( esc_url_raw( $this->icon ), '"' ) . '");' ) . '">', $str );
+ }
+
+ public function process_request( $post, array $post_data ) {
+ $url = str_replace( '&amp;', '&', $this->url );
+ $url = str_replace( '%post_id%', rawurlencode( $post->ID ), $url );
+ $url = str_replace( '%post_url%', rawurlencode( $this->get_share_url( $post->ID ) ), $url );
+ $url = str_replace( '%post_full_url%', rawurlencode( get_permalink( $post->ID ) ), $url );
+ $url = str_replace( '%post_title%', rawurlencode( $this->get_share_title( $post->ID ) ), $url );
+ $url = str_replace( '%home_url%', rawurlencode( home_url() ), $url );
+ $url = str_replace( '%post_slug%', rawurlencode( $post->post_name ), $url );
+
+ if ( strpos( $url, '%post_tags%' ) !== false ) {
+ $tags = get_the_tags( $post->ID );
+ $tagged = '';
+
+ if ( $tags ) {
+ $tagged_raw = array();
+ foreach ( $tags as $tag ) {
+ $tagged_raw[] = rawurlencode( $tag->name );
+ }
+
+ $tagged = implode( ',', $tagged_raw );
+ }
+
+ $url = str_replace( '%post_tags%', $tagged, $url );
+ }
+
+ if ( strpos( $url, '%post_excerpt%' ) !== false ) {
+ $url_excerpt = $post->post_excerpt;
+ if ( empty( $url_excerpt ) ) {
+ $url_excerpt = $post->post_content;
+ }
+
+ $url_excerpt = strip_tags( strip_shortcodes( $url_excerpt ) );
+ $url_excerpt = wp_html_excerpt( $url_excerpt, 100 );
+ $url_excerpt = rtrim( preg_replace( '/[^ .]*$/', '', $url_excerpt ) );
+ $url = str_replace( '%post_excerpt%', rawurlencode( $url_excerpt ), $url );
+ }
+
+ // Record stats
+ parent::process_request( $post, $post_data );
+
+ // Redirect
+ wp_redirect( $url );
+ die();
+ }
+
+ public function display_options() {
+?>
+<div class="input">
+ <table class="form-table">
+ <tbody>
+ <tr>
+ <th scope="row"><?php _e( 'Label', 'jetpack' ); ?></th>
+ <td><input type="text" name="name" value="<?php echo esc_attr( $this->name ); ?>" /></td>
+ </tr>
+
+ <tr>
+ <th scope="row"><?php _e( 'URL', 'jetpack' ); ?></th>
+ <td><input type="text" name="url" value="<?php echo esc_attr( $this->url ); ?>" /></td>
+ </tr>
+
+ <tr>
+ <th scope="row"><?php _e( 'Icon', 'jetpack' ); ?></th>
+ <td><input type="text" name="icon" value="<?php echo esc_attr( $this->icon ); ?>" /></td>
+ </tr>
+
+ <tr>
+ <th scope="row"></th>
+ <td>
+ <input class="button-secondary" type="submit" value="<?php esc_attr_e( 'Save', 'jetpack' ); ?>" />
+ <a href="#" class="remove"><small><?php _e( 'Remove Service', 'jetpack' ); ?></small></a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+<?php
+ }
+
+ public function update_options( array $data ) {
+ $name = trim( wp_html_excerpt( wp_kses( stripslashes( $data['name'] ), array() ), 30 ) );
+ $url = trim( esc_url_raw( $data['url'] ) );
+ $icon = trim( esc_url_raw( $data['icon'] ) );
+
+ if ( $name ) {
+ $this->name = $name;
+ }
+
+ if ( $url ) {
+ $this->url = $url;
+ }
+
+ if ( $icon ) {
+ $this->icon = $icon;
+ }
+ }
+
+ public function get_options() {
+ return array(
+ 'name' => $this->name,
+ 'icon' => $this->icon,
+ 'url' => $this->url,
+ );
+ }
+
+ public function display_preview( $echo = true, $force_smart = false, $button_style = null ) {
+ $opts = $this->get_options();
+
+ $text = '&nbsp;';
+ if ( ! $this->smart ) {
+ if ( $this->button_style != 'icon' ) {
+ $text = $this->get_name();
+ }
+ }
+
+ $klasses = array( 'share-' . $this->shortname );
+
+ if ( $this->button_style == 'icon' || $this->button_style == 'icon-text' ) {
+ $klasses[] = 'share-icon';
+ }
+
+ if ( $this->button_style == 'icon' ) {
+ $text = '';
+ $klasses[] = 'no-text';
+ }
+
+ if ( $this->button_style == 'text' ) {
+ $klasses[] = 'no-icon';
+ }
+
+ $link = sprintf(
+ '<a rel="nofollow" class="%s" href="javascript:void(0)" title="%s"><span style="background-image:url(&quot;%s&quot;) !important;background-position:left center;background-repeat:no-repeat;">%s</span></a>',
+ implode( ' ', $klasses ),
+ $this->get_name(),
+ addcslashes( esc_url_raw( $opts['icon'] ), '"' ),
+ $text
+ );
+ ?>
+ <div class="option option-smart-off">
+ <?php echo $link ; ?>
+ </div><?php
+ }
+}
+
+class Share_Tumblr extends Sharing_Source {
+ public $shortname = 'tumblr';
+ public $icon = '\f214';
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+ }
+
+ public function get_name() {
+ return __( 'Tumblr', 'jetpack' );
+ }
+
+ public function get_display( $post ) {
+ if ( $this->smart ) {
+ $target = '';
+ if ( true == $this->open_link_in_new ) {
+ $target = '_blank';
+ }
+
+ /**
+ * If we are looking at a single post, let Tumblr figure out the post type (text, photo, link, quote, chat, or video)
+ * based on the content available on the page.
+ * If we are not looking at a single post, content from other posts can appear on the page and Tumblr will pick that up.
+ * In this case, we want Tumblr to focus on our current post, so we will limit the post type to link, where we can give Tumblr a link to our post.
+ */
+ if ( ! is_single() ) {
+ $posttype = 'data-posttype="link"';
+ } else {
+ $posttype = '';
+ }
+
+ // Documentation: https://www.tumblr.com/docs/en/share_button
+ return sprintf(
+ '<a class="tumblr-share-button" target="%1$s" href="%2$s" data-title="%3$s" data-content="%4$s" title="%5$s"%6$s>%5$s</a>',
+ $target,
+ 'https://www.tumblr.com/share',
+ $this->get_share_title( $post->ID ),
+ $this->get_share_url( $post->ID ),
+ __( 'Share on Tumblr', 'jetpack' ),
+ $posttype
+ );
+ } else {
+ return $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'Tumblr', 'share to', 'jetpack' ), __( 'Click to share on Tumblr', 'jetpack' ), 'share=tumblr' );
+ }
+ }
+
+ public function process_request( $post, array $post_data ) {
+ // Record stats
+ parent::process_request( $post, $post_data );
+
+ // Redirect to Tumblr's sharing endpoint (a la their bookmarklet)
+ $url = 'https://www.tumblr.com/share?v=3&u=' . rawurlencode( $this->get_share_url( $post->ID ) ) . '&t=' . rawurlencode( $this->get_share_title( $post->ID ) ) . '&s=';
+ wp_redirect( $url );
+ die();
+ }
+
+ public function display_footer() {
+ if ( $this->smart ) {
+ ?><script id="tumblr-js" type="text/javascript" src="https://assets.tumblr.com/share-button.js"></script><?php
+ } else {
+ $this->js_dialog( $this->shortname, array( 'width' => 450, 'height' => 450 ) );
+ }
+ }
+}
+
+class Share_Pinterest extends Sharing_Source {
+ public $shortname = 'pinterest';
+ public $icon = '\f209';
+
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+ }
+
+ public function get_name() {
+ return __( 'Pinterest', 'jetpack' );
+ }
+
+ public function get_image( $post ) {
+ if ( class_exists( 'Jetpack_PostImages' ) ) {
+ $image = Jetpack_PostImages::get_image( $post->ID, array( 'fallback_to_avatars' => true ) );
+ if ( ! empty( $image ) ) {
+ return $image['src'];
+ }
+ }
+
+ /**
+ * Filters the default image used by the Pinterest Pin It share button.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.6.0
+ *
+ * @param string $url Default image URL.
+ */
+ return apply_filters( 'jetpack_sharing_pinterest_default_image', 'https://s0.wp.com/i/blank.jpg' );
+ }
+
+ public function get_external_url( $post ) {
+ $url = 'https://www.pinterest.com/pin/create/button/?url=' . rawurlencode( $this->get_share_url( $post->ID ) ) . '&media=' . rawurlencode( $this->get_image( $post ) ) . '&description=' . rawurlencode( $post->post_title );
+
+ /**
+ * Filters the Pinterest share URL used in sharing button output.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.6.0
+ *
+ * @param string $url Pinterest share URL.
+ */
+ return apply_filters( 'jetpack_sharing_pinterest_share_url', $url );
+ }
+
+ public function get_widget_type() {
+ /**
+ * Filters the Pinterest widget type.
+ *
+ * @see https://business.pinterest.com/en/widget-builder
+ *
+ * @module sharedaddy
+ *
+ * @since 3.6.0
+ *
+ * @param string $type Pinterest widget type. Default of 'buttonPin' for single-image selection. 'buttonBookmark' for multi-image modal.
+ */
+ return apply_filters( 'jetpack_sharing_pinterest_widget_type', 'buttonPin' );
+ }
+
+ public function get_display( $post ) {
+ $display = '';
+
+ if ( $this->smart ) {
+ $display = sprintf(
+ '<div class="pinterest_button"><a href="%s" data-pin-do="%s" data-pin-config="beside"><img src="//assets.pinterest.com/images/pidgets/pinit_fg_en_rect_gray_20.png" /></a></div>',
+ esc_url( $this->get_external_url( $post ) ),
+ esc_attr( $this->get_widget_type() )
+ );
+ } else {
+ $display = $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'Pinterest', 'share to', 'jetpack' ), __( 'Click to share on Pinterest', 'jetpack' ), 'share=pinterest', 'sharing-pinterest-' . $post->ID );
+ }
+
+ /** This filter is already documented in modules/sharedaddy/sharing-sources.php */
+ if ( apply_filters( 'jetpack_register_post_for_share_counts', true, $post->ID, 'linkedin' ) ) {
+ sharing_register_post_for_share_counts( $post->ID );
+ }
+
+ return $display;
+ }
+
+ public function process_request( $post, array $post_data ) {
+ // Record stats
+ parent::process_request( $post, $post_data );
+ // If we're triggering the multi-select panel, then we don't need to redirect to Pinterest
+ if ( ! isset( $_GET['js_only'] ) ) {
+ $pinterest_url = esc_url_raw( $this->get_external_url( $post ) );
+ wp_redirect( $pinterest_url );
+ } else {
+ echo '// share count bumped';
+ }
+ die();
+ }
+
+ public function display_footer() {
+ /**
+ * Filter the Pin it button appearing when hovering over images when using the official button style.
+ *
+ * @module sharedaddy
+ *
+ * @since 3.6.0
+ *
+ * @param bool $jetpack_pinit_over True by default, displays the Pin it button when hovering over images.
+ */
+ $jetpack_pinit_over = apply_filters( 'jetpack_pinit_over_button', true );
+ ?>
+ <?php if ( $this->smart ) : ?>
+ <script type="text/javascript">
+ // Pinterest shared resources
+ var s = document.createElement("script");
+ s.type = "text/javascript";
+ s.async = true;
+ <?php if ( $jetpack_pinit_over ) {
+ echo "s.setAttribute('data-pin-hover', true);";
+ } ?>
+ s.src = window.location.protocol + "//assets.pinterest.com/js/pinit.js";
+ var x = document.getElementsByTagName("script")[0];
+ x.parentNode.insertBefore(s, x);
+ // if 'Pin it' button has 'counts' make container wider
+ jQuery(window).load( function(){ jQuery( 'li.share-pinterest a span:visible' ).closest( '.share-pinterest' ).width( '80px' ); } );
+ </script>
+ <?php elseif ( 'buttonPin' != $this->get_widget_type() ) : ?>
+ <script type="text/javascript">
+ jQuery(document).ready( function(){
+ jQuery('body').on('click', 'a.share-pinterest', function(e){
+ e.preventDefault();
+ // Load Pinterest Bookmarklet code
+ var s = document.createElement("script");
+ s.type = "text/javascript";
+ s.src = window.location.protocol + "//assets.pinterest.com/js/pinmarklet.js?r=" + ( Math.random() * 99999999 );
+ var x = document.getElementsByTagName("script")[0];
+ x.parentNode.insertBefore(s, x);
+ // Trigger Stats
+ var s = document.createElement("script");
+ s.type = "text/javascript";
+ s.src = this + ( this.toString().indexOf( '?' ) ? '&' : '?' ) + 'js_only=1';
+ var x = document.getElementsByTagName("script")[0];
+ x.parentNode.insertBefore(s, x);
+ });
+ });
+ </script>
+ <?php endif;
+ }
+}
+
+class Share_Pocket extends Sharing_Source {
+ public $shortname = 'pocket';
+ public $icon = '\f224';
+
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+ }
+
+ public function get_name() {
+ return __( 'Pocket', 'jetpack' );
+ }
+
+ public function process_request( $post, array $post_data ) {
+ // Record stats
+ parent::process_request( $post, $post_data );
+
+ $pocket_url = esc_url_raw( 'https://getpocket.com/save/?url=' . rawurlencode( $this->get_share_url( $post->ID ) ) . '&title=' . rawurlencode( $this->get_share_title( $post->ID ) ) );
+ wp_redirect( $pocket_url );
+ exit;
+ }
+
+ public function get_display( $post ) {
+ if ( $this->smart ) {
+ $post_count = 'horizontal';
+
+ $button = '';
+ $button .= '<div class="pocket_button">';
+ $button .= sprintf( '<a href="https://getpocket.com/save" class="pocket-btn" data-lang="%s" data-save-url="%s" data-pocket-count="%s" >%s</a>', 'en', esc_attr( $this->get_share_url( $post->ID ) ), $post_count, esc_attr__( 'Pocket', 'jetpack' ) );
+ $button .= '</div>';
+
+ return $button;
+ } else {
+ return $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'Pocket', 'share to', 'jetpack' ), __( 'Click to share on Pocket', 'jetpack' ), 'share=pocket' );
+ }
+
+ }
+
+ function display_footer() {
+ if ( $this->smart ) :
+ ?>
+ <script>
+ // Don't use Pocket's default JS as it we need to force init new Pocket share buttons loaded via JS.
+ function jetpack_sharing_pocket_init() {
+ jQuery.getScript( 'https://widgets.getpocket.com/v1/j/btn.js?v=1' );
+ }
+ jQuery( document ).ready( jetpack_sharing_pocket_init );
+ jQuery( document.body ).on( 'post-load', jetpack_sharing_pocket_init );
+ </script>
+ <?php
+ else :
+ $this->js_dialog( $this->shortname, array( 'width' => 450, 'height' => 450 ) );
+ endif;
+
+ }
+
+}
+
+class Share_Telegram extends Sharing_Source {
+ public $shortname = 'telegram';
+
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+ }
+
+ public function get_name() {
+ return __( 'Telegram', 'jetpack' );
+ }
+ public function process_request( $post, array $post_data ) {
+ // Record stats
+ parent::process_request( $post, $post_data );
+ $telegram_url = esc_url_raw( 'https://telegram.me/share/url?url=' . rawurlencode( $this->get_share_url( $post->ID ) ) . '&text=' . rawurlencode( $this->get_share_title( $post->ID ) ) );
+ wp_redirect( $telegram_url );
+ exit;
+ }
+
+ public function get_display( $post ) {
+ return $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'Telegram', 'share to', 'jetpack' ), __( 'Click to share on Telegram', 'jetpack' ), 'share=telegram' );
+ }
+
+ function display_footer() {
+ $this->js_dialog( $this->shortname, array( 'width' => 450, 'height' => 450 ) );
+ }
+}
+
+class Jetpack_Share_WhatsApp extends Sharing_Source {
+ public $shortname = 'jetpack-whatsapp';
+
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+ }
+
+ public function get_name() {
+ return __( 'WhatsApp', 'jetpack' );
+ }
+
+ public function get_display( $post ) {
+ return $this->get_link( $this->get_process_request_url( $post->ID ), _x( 'WhatsApp', 'share to', 'jetpack' ), __( 'Click to share on WhatsApp', 'jetpack' ), 'share=jetpack-whatsapp' );
+ }
+
+ public function process_request( $post, array $post_data ) {
+ // Record stats
+ parent::process_request( $post, $post_data );
+ $url = 'https://api.whatsapp.com/send?text=' . rawurlencode( $this->get_share_title( $post->ID ) . ' ' . $this->get_share_url( $post->ID ) );
+ wp_redirect( $url );
+ exit;
+ }
+}
+
+class Share_Skype extends Sharing_Source {
+ public $shortname = 'skype';
+ public $icon = '\f220';
+ private $share_type = 'default';
+
+ public function __construct( $id, array $settings ) {
+ parent::__construct( $id, $settings );
+
+ if ( isset( $settings['share_type'] ) ) {
+ $this->share_type = $settings['share_type'];
+ }
+
+ if ( 'official' == $this->button_style ) {
+ $this->smart = true;
+ } else {
+ $this->smart = false;
+ }
+
+ }
+
+ public function get_name() {
+ return __( 'Skype', 'jetpack' );
+ }
+
+ public function get_display( $post ) {
+ if ( $this->smart ) {
+ $skype_share_html = sprintf(
+ '<div class="skype-share" data-href="%1$s" data-lang="%2$s" data-style="small" data-source="jetpack" ></div>',
+ esc_attr( $this->get_share_url( $post->ID ) ),
+ 'en-US'
+ );
+ return $skype_share_html;
+ }
+
+ /** This filter is already documented in modules/sharedaddy/sharing-sources.php */
+ if ( apply_filters( 'jetpack_register_post_for_share_counts', true, $post->ID, 'skype' ) ) {
+ sharing_register_post_for_share_counts( $post->ID );
+ }
+ return $this->get_link(
+ $this->get_process_request_url( $post->ID ), _x( 'Skype', 'share to', 'jetpack' ), __( 'Click to share on Skype', 'jetpack' ), 'share=skype', 'sharing-skype-' . $post->ID );
+ }
+
+ public function process_request( $post, array $post_data ) {
+ $skype_url = sprintf(
+ 'https://web.skype.com/share?url=%1$s&lang=%2$s=&source=jetpack',
+ rawurlencode( $this->get_share_url( $post->ID ) ),
+ 'en-US'
+ );
+
+ // Record stats
+ parent::process_request( $post, $post_data );
+
+ // Redirect to Skype
+ wp_redirect( $skype_url );
+ die();
+ }
+
+ public function display_footer() {
+ if ( $this->smart ) :
+ ?>
+ <script>
+ (function(r, d, s) {
+ r.loadSkypeWebSdkAsync = r.loadSkypeWebSdkAsync || function(p) {
+ var js, sjs = d.getElementsByTagName(s)[0];
+ if (d.getElementById(p.id)) { return; }
+ js = d.createElement(s);
+ js.id = p.id;
+ js.src = p.scriptToLoad;
+ js.onload = p.callback
+ sjs.parentNode.insertBefore(js, sjs);
+ };
+ var p = {
+ scriptToLoad: 'https://swx.cdn.skype.com/shared/v/latest/skypewebsdk.js',
+ id: 'skype_web_sdk'
+ };
+ r.loadSkypeWebSdkAsync(p);
+ })(window, document, 'script');
+ </script>
+ <?php
+ else :
+ $this->js_dialog( $this->shortname, array( 'width' => 305, 'height' => 665 ) );
+ endif;
+ }
+}
diff --git a/plugins/jetpack/modules/sharedaddy/sharing.css b/plugins/jetpack/modules/sharedaddy/sharing.css
new file mode 100644
index 00000000..7481f416
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/sharing.css
@@ -0,0 +1,755 @@
+/**
+ * Sharedaddy Base Styles
+ *
+ * Contains styles for modules, containers, buttons
+ */
+
+
+/* Master container */
+#jp-post-flair {
+ padding-top: .5em;
+}
+
+
+/* Overall Sharedaddy block title */
+div.sharedaddy,
+#content div.sharedaddy,
+#main div.sharedaddy {
+ clear: both;
+}
+
+div.sharedaddy h3.sd-title {
+ margin: 0 0 1em 0;
+ display: inline-block;
+ line-height: 1.2;
+ font-size: 9pt;
+ font-weight: bold;
+}
+
+div.sharedaddy h3.sd-title:before {
+ content: "";
+ display: block;
+ width: 100%;
+ min-width: 30px;
+ border-top: 1px solid #ddd;
+ margin-bottom: 1em;
+}
+
+body.highlander-light h3.sd-title:before {
+ border-top: 1px solid rgba(0,0,0,.2);
+}
+
+body.highlander-dark h3.sd-title:before {
+ border-top: 1px solid rgba(255,255,255,.4);
+}
+
+
+/* Sharing services list */
+.sd-content ul {
+ padding: 0 !important;
+ margin: 0 0 .7em 0 !important;
+ list-style: none !important;
+}
+
+.sd-content ul li {
+ display: inline-block;
+}
+
+.sd-content ul li.share-deprecated {
+ opacity: 0.5;
+}
+
+.sd-content ul li.share-deprecated a span {
+ text-decoration: line-through;
+}
+
+.sd-block.sd-gplus {
+ margin: 0 0 .5em 0;
+}
+
+.sd-gplus .sd-content {
+ font-size: 12px;
+}
+
+
+/* Buttons */
+.sd-social-icon .sd-content ul li a.sd-button,
+.sd-social-text .sd-content ul li a.sd-button,
+.sd-content ul li a.sd-button,
+.sd-content ul li .option a.share-ustom, /* Ugh. */
+.sd-content ul li.preview-item div.option.option-smart-off a,
+.sd-content ul li.advanced a.share-more,
+.sd-social-icon-text .sd-content ul li a.sd-button,
+.sd-social-official .sd-content>ul>li>a.sd-button,
+#sharing_email .sharing_send,
+.sd-social-official .sd-content>ul>li .digg_button >a { /* official Digg button no longer works, needs cleaning */
+ text-decoration: none !important;
+ display: inline-block;
+ font-size: 12px;
+ font-family: "Open Sans", sans-serif;
+ font-weight: normal;
+ border-radius: 3px;
+ color: #545454 !important;
+ background: #f8f8f8;
+ border: 1px solid #cccccc;
+ box-shadow: 0 1px 0 rgba(0,0,0,.08);
+ text-shadow: none;
+ line-height: 23px;
+ padding: 1px 8px 0px 5px;
+}
+
+.sd-social-text .sd-content ul li a.sd-button span,
+.sd-content ul li a.sd-button>span,
+.sd-content ul li .option a.share-ustom span, /* Ugh. */
+.sd-content ul li.preview-item div.option.option-smart-off a span,
+.sd-content ul li.advanced a.share-more span,
+.sd-social-icon-text .sd-content ul li a.sd-button>span,
+.sd-social-official .sd-content>ul>li>a.sd-button span,
+.sd-social-official .sd-content>ul>li .digg_button >a span { /* official Digg button no longer works, needs cleaning */
+ line-height: 23px;
+}
+
+
+
+/* Our gray buttons should be smaller when seen with the official ones */
+.sd-social-official .sd-content>ul>li>a.sd-button,
+.sd-social-official .sd-content .sharing-hidden .inner>ul>li>a.sd-button,
+.sd-social-official .sd-content>ul>li .digg_button>a,
+.sd-social-official .sd-content .sharing-hidden .inner>ul>li .digg_button>a {
+ line-height: 17px;
+ box-shadow: none; /* No shadow on gray buttons between the official ones */
+ vertical-align: top;
+}
+
+.sd-social-official .sd-content ul li a.sd-button>span {
+ line-height: 17px;
+}
+.sd-social-official .sd-content>ul>li>a.sd-button:before,
+.sd-social-official .sd-content>ul>li .digg_button>a:before,
+.sd-social-official .sd-content .sharing-hidden .inner>ul>li>a.sd-button:before,
+.sd-social-official .sd-content .sharing-hidden .inner>ul>li .digg_button>a:before {
+ margin-bottom: -1px;
+}
+
+.sd-social-icon .sd-content ul li a.sd-button:hover,
+.sd-social-icon .sd-content ul li a.sd-button:active,
+.sd-social-text .sd-content ul li a.sd-button:hover,
+.sd-social-text .sd-content ul li a.sd-button:active,
+.sd-social-icon-text .sd-content ul li a.sd-button:hover,
+.sd-social-icon-text .sd-content ul li a.sd-button:active,
+.sd-social-official .sd-content>ul>li>a.sd-button:hover,
+.sd-social-official .sd-content>ul>li>a.sd-button:active,
+.sd-social-official .sd-content>ul>li .digg_button>a:hover,
+.sd-social-official .sd-content>ul>li .digg_button>a:active {
+ color: #555;
+ background: #fafafa;
+ border: 1px solid #999999;
+}
+
+.sd-social-icon .sd-content ul li a.sd-button:active,
+.sd-social-text .sd-content ul li a.sd-button:active,
+.sd-social-icon-text .sd-content ul li a.sd-button:active,
+.sd-social-official .sd-content>ul>li>a.sd-button:active,
+.sd-social-official .sd-content>ul>li .digg_button>a:active {
+ box-shadow: inset 0 1px 0 rgba(0,0,0,.16);
+}
+
+/* All icons */
+.sd-content ul li a.sd-button:before {
+ display: inline-block;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font: normal 18px/1 'social-logos';
+ vertical-align: top;
+ text-align: center;
+}
+/* text + icon styles should have relative and top position */
+.sd-social-icon-text ul li a.sd-button:before {
+ position: relative;
+ top: 2px;
+}
+
+/* Make it look great in Chrome and Safari */
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ .sd-content ul li a.sd-button:before {
+ position: relative;
+ top: 2px;
+ }
+}
+
+.sd-social-official ul li a.sd-button:before {
+ position: relative;
+ top: -2px;
+}
+/* Make it look great in Chrome and Safari */
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ .sd-social-official ul li a.sd-button:before {
+ top: 0px;
+ }
+}
+
+.sd-content ul li {
+ margin: 0 5px 5px 0;
+ padding: 0;
+}
+/* Add more pading on touch devices */
+.jp-sharing-input-touch .sd-content ul li { padding-left: 10px; }
+
+/* Text + icon & Official */
+.sd-social-icon-text .sd-content ul li a span,
+.sd-social-official .sd-content ul li a.sd-button span,
+.sd-content ul li.preview-item a.sd-button span {
+ margin-left: 3px;
+}
+.sd-content ul li.preview-item.no-icon a.sd-button span {
+ margin-left: 0;
+}
+
+/* Text only */
+.sd-social-text .sd-content ul li a:before,
+.sd-content ul li.no-icon a:before {
+ display: none;
+}
+body .sd-social-text .sd-content ul li.share-custom a span,
+body .sd-content ul li.share-custom.no-icon a span {
+ background-image: none;
+ background-position: -500px -500px !important; /* hack to work around !important inline style */
+ background-repeat: no-repeat !important;
+ padding-left: 0;
+ height: 0;
+ line-height: inherit;
+}
+
+.sd-social-icon .sd-content ul li a.share-more {
+ position: relative;
+ top: -4px;
+}
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ .sd-social-icon .sd-content ul li a.share-more {
+ top: 2px;
+ }
+}
+/* Firefox specific hack to make the share more button look better on Firefox. */
+@-moz-document url-prefix() {
+ .sd-social-icon .sd-content ul li a.share-more {
+ top: 2px;
+ }
+}
+
+.sd-social-icon .sd-content ul li a.share-more span {
+ margin-left: 3px;
+}
+
+
+/* Individual icons */
+.sd-social-icon .sd-content ul li.share-print a:before,
+.sd-social-text .sd-content ul li.share-print a:before,
+.sd-content ul li.share-print div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-print a:before,
+.sd-social-official .sd-content li.share-print a:before {
+ content: '\f469';
+}
+
+.sd-social-icon .sd-content ul li.share-email a:before,
+.sd-social-text .sd-content ul li.share-email a:before,
+.sd-content ul li.share-email div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-email a:before,
+.sd-social-official .sd-content li.share-email a:before {
+ content: '\f410';
+}
+.sd-social-icon .sd-content ul li.share-linkedin a:before,
+.sd-social-text .sd-content ul li.share-linkedin a:before,
+.sd-content ul li.share-linkedin div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-linkedin a:before {
+ content: '\f207';
+}
+.sd-social-icon .sd-content ul li.share-twitter a:before,
+.sd-social-text .sd-content ul li.share-twitter a:before,
+.sd-content ul li.share-twitter div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-twitter a:before {
+ content: '\f202';
+}
+.sd-social-icon .sd-content ul li.share-reddit a:before,
+.sd-social-text .sd-content ul li.share-reddit a:before,
+.sd-content ul li.share-reddit div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-reddit a:before {
+ content: '\f222';
+}
+.sd-social-icon .sd-content ul li.share-tumblr a:before,
+.sd-social-text .sd-content ul li.share-tumblr a:before,
+.sd-content ul li.share-tumblr div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-tumblr a:before {
+ content: '\f607';
+}
+
+.sd-social-icon .sd-content ul li.share-pocket a:before,
+.sd-social-text .sd-content ul li.share-pocket a:before,
+.sd-content ul li.share-pocket div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-pocket a:before {
+ content: '\f224';
+}
+.sd-social-icon .sd-content ul li.share-pinterest a:before,
+.sd-social-text .sd-content ul li.share-pinterest a:before,
+.sd-content ul li.share-pinterest div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-pinterest a:before {
+ content: '\f210';
+}
+.sd-social-icon .sd-content ul li.share-facebook a:before,
+.sd-social-text .sd-content ul li.share-facebook a:before,
+.sd-content ul li.share-facebook div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-facebook a:before {
+ content: '\f203';
+}
+.sd-social-icon .sd-content ul li.share-press-this a:before,
+.sd-social-text .sd-content ul li.share-press-this a:before,
+.sd-content ul li.share-press-this div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-press-this a:before,
+.sd-social-official .sd-content li.share-press-this a:before {
+ content: '\f205';
+}
+.sd-social-official .sd-content li.share-press-this a:before {
+ color: #2ba1cb;
+}
+.sd-social-icon .sd-content ul li.share-telegram a:before,
+.sd-social-text .sd-content ul li.share-telegram a:before,
+.sd-content ul li.share-telegram div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-telegram a:before,
+.sd-social-official .sd-content li.share-telegram a:before {
+ content: '\f606';
+}
+.sd-social-official .sd-content li.share-telegram a:before {
+ color: #0088cc;
+}
+.sd-social-icon .sd-content ul li.share-skype a:before,
+.sd-social-text .sd-content ul li.share-skype a:before,
+.sd-content ul li.share-skype div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-skype a:before {
+ content: '\f220';
+}
+.sd-social-icon .sd-content ul a.share-more:before,
+.sd-social-text .sd-content ul a.share-more:before,
+.sd-content ul li.advanced a.share-more:before,
+.sd-social-icon-text .sd-content a.share-more:before,
+.sd-social-official .sd-content a.share-more:before {
+ content: '\f415';
+}
+.sd-social-official .sd-content a.share-more:before {
+ color: #2ba1cb;
+}
+
+.sd-social-icon .sd-content ul li.share-jetpack-whatsapp a:before,
+.sd-social-text .sd-content ul li.share-jetpack-whatsapp a:before,
+.sd-content ul li.share-jetpack-whatsapp div.option.option-smart-off a:before,
+.sd-social-icon-text .sd-content li.share-jetpack-whatsapp a:before,
+.sd-social-official .sd-content li.share-jetpack-whatsapp a:before {
+ content: '\f608';
+}
+.sd-social-official .sd-content li.share-jetpack-whatsapp a:before {
+ color: #43d854;
+}
+.sd-social-icon .sd-content ul li[class*='share-'].share-jetpack-whatsapp a.sd-button {
+ background: #43d854;
+ color: #fff !important;
+}
+
+
+.sd-social-icon .sd-content ul li.share-deprecated a:before,
+.sd-social-icon-text .sd-content li.share-deprecated a:before,
+.sd-social-official .sd-content li.share-deprecated a:before,
+.sd-content ul li.share-deprecated div.option.option-smart-off a:before {
+ width: 1em;
+ height: 1em;
+ content: "\1F6AB";
+}
+
+/* Share count */
+.sd-social .sd-button .share-count {
+ background: #2ea2cc;
+ color: #fff;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+ display: inline-block;
+ text-align: center;
+ font-size: 10px;
+ padding: 1px 3px;
+ line-height: 1;
+}
+
+
+/* Official buttons */
+.sd-social-official .sd-content ul, .sd-social-official .sd-content ul li {
+ line-height: 25px !important;
+}
+
+.sd-social-official .sd-content>ul>li>a.sd-button span {
+ line-height: 1;
+}
+
+.sd-social-official .sd-content ul:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+.sd-social-official .sd-content li.share-press-this a {
+ margin: 0 0 5px 0;
+}
+
+.sd-social-official .sd-content ul>li {
+ display: block;
+ float: left;
+ margin: 0 10px 5px 0 !important;
+ height: 25px;
+}
+
+.sd-social-official .fb-share-button > span {
+ vertical-align: top !important;
+}
+
+.sd-social-official .sd-content .pocket_button iframe {
+ width: 98px;
+}
+
+
+/* Individual official buttons */
+.reddit_button iframe {
+ margin-top: 1px;
+}
+
+.pocket_button iframe, .pinterest_button, .twitter_button, .linkedin_button>span {
+ margin: 0 !important;
+}
+
+.linkedin_button>span, .pinterest_button a {
+ display: block !important;
+}
+
+.sd-social-official .sd-content .share-skype {
+ width: 55px;
+}
+
+body .sd-social-official li.share-print ,
+body .sd-social-official li.share-email a,
+body .sd-social-official li.share-custom a,
+body .sd-social-official li a.share-more,
+body .sd-social-official li.share-digg a,
+body .sd-social-official li.share-press-this a
+{
+ position: relative;
+ top: 0;
+}
+
+
+/* Custom icons */
+body .sd-social-icon .sd-content li.share-custom>a {
+ padding: 2px 3px 0 3px;
+ position: relative;
+ top: 4px;
+}
+
+body .sd-social-icon .sd-content li.share-custom a span,
+body .sd-social-icon-text .sd-content li.share-custom a span,
+body .sd-social-text .sd-content li.share-custom a span,
+body .sd-social-official .sd-content li.share-custom a span,
+body .sd-content ul li.share-custom a.share-icon span
+{
+ background-size: 16px 16px;
+ background-repeat: no-repeat;
+ margin-left: 0;
+ padding: 0 0 0 19px;
+ display: inline-block;
+ height: 21px;
+ line-height: 16px;
+}
+
+body .sd-social-icon .sd-content li.share-custom a span {
+ width: 0;
+}
+
+body .sd-social-icon .sd-content li.share-custom a span {
+ padding-left: 16px !important;
+}
+
+
+/* Overflow Sharing dialog */
+.sharing-hidden .inner {
+ position: absolute;
+ z-index: 2;
+ border: 1px solid #ccc;
+ padding: 10px;
+ background: #fff;
+ box-shadow: 0px 5px 20px rgba(0,0,0,.2);
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ border-radius: 2px;
+ margin-top: 5px;
+ max-width: 400px;
+}
+
+.sharing-hidden .inner ul{
+ margin: 0 !important;
+}
+
+.sd-social-official .sd-content .sharing-hidden ul>li.share-end {
+ clear: both;
+ margin: 0 !important;
+ height: 0 !important;
+}
+
+.sharing-hidden .inner:before, .sharing-hidden .inner:after {
+ position: absolute;
+ z-index: 1;
+ top: -8px;
+ left: 20px;
+ width: 0;
+ height: 0;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-bottom: 8px solid #ccc;
+ content: "";
+ display: block;
+}
+
+.sharing-hidden .inner:after {
+ z-index: 2;
+ top: -7px;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-bottom: 8px solid #fff;
+}
+
+.sharing-hidden ul {
+ margin: 0;
+}
+
+
+/**
+ * Special colorful look for "Icon Only" option
+ */
+
+.sd-social-icon .sd-content ul li[class*='share-'] a,
+.sd-social-icon .sd-content ul li[class*='share-'] a:hover,
+.sd-social-icon .sd-content ul li[class*='share-'] div.option a {
+ border-radius: 50%;
+ -webkit-border-radius: 50%;
+ border: 0;
+ box-shadow: none;
+ padding: 7px;
+ position: relative;
+ top: -2px;
+ line-height: 1;
+ width: auto;
+ height: auto;
+ margin-bottom: 0;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'] a.sd-button>span,
+.sd-social-icon .sd-content ul li[class*='share-'] div.option a span {
+ line-height: 1;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'] a:hover,
+.sd-social-icon .sd-content ul li[class*='share-'] div.option a:hover {
+ border: none;
+ opacity: .6;
+}
+
+
+
+.sd-social-icon .sd-content ul li[class*='share-'] a.sd-button:before {
+ top: 1px;
+ top: 0px\9; /* IE8 and below */
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'] a.sd-button.share-custom {
+ padding: 8px 8px 6px 8px;
+ top: 5px;
+}
+
+.sd-social-icon .sd-content ul li a.sd-button.share-more {
+ margin-left: 10px;
+}
+
+.sd-social-icon .sd-content ul li:first-child a.sd-button.share-more {
+ margin-left: 0;
+}
+
+
+.sd-social-icon .sd-button span.share-count {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ border-radius: 0;
+ background: #555;
+ font-size: 9px;
+}
+
+/* Special look colors */
+.sd-social-icon .sd-content ul li[class*='share-'] a.sd-button {
+ background: #e9e9e9;
+ margin-top: 2px;
+ text-indent: 0;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-tumblr a.sd-button {
+ background: #2c4762;
+ color: #fff !important;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-facebook a.sd-button {
+ background: #3b5998;
+ color: #fff !important;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-twitter a.sd-button {
+ background: #00acee;
+ color: #fff !important;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-pinterest a.sd-button {
+ background: #ca1f27;
+ color: #fff !important;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-digg a.sd-button {
+ color: #555555 !important;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-press-this a.sd-button {
+ background: #1e8cbe;
+ color: #fff !important;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-telegram a.sd-button {
+ background: #0088cc;
+ color: #fff !important;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-linkedin a.sd-button {
+ background: #0077b5;
+ color: #fff !important;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-pocket a.sd-button {
+ background: #ee4056;
+ color: #fff !important;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-reddit a.sd-button {
+ background: #cee3f8;
+ color: #555555 !important;
+}
+
+.sd-social-icon .sd-content ul li[class*='share-'].share-skype a.sd-button {
+ background: #00AFF0;
+ color: #fff !important;
+}
+
+/**
+ * Screen Reader Text for "Icon Only" option
+ */
+
+.sharing-screen-reader-text {
+ clip: rect(1px, 1px, 1px, 1px);
+ position: absolute !important;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+}
+
+.sharing-screen-reader-text:hover,
+.sharing-screen-reader-text:active,
+.sharing-screen-reader-text:focus {
+ background-color: #f1f1f1;
+ border-radius: 3px;
+ box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);
+ clip: auto !important;
+ color: #21759b;
+ display: block;
+ font-size: 14px;
+ font-weight: bold;
+ height: auto;
+ left: 5px;
+ line-height: normal;
+ padding: 15px 23px 14px;
+ text-decoration: none;
+ top: 5px;
+ width: auto;
+ z-index: 100000; /* Above WP toolbar */
+}
+
+
+/**
+ * Sharing Email Dialog
+ */
+
+#sharing_email {
+ width: 342px;
+ position: absolute;
+ z-index: 1001;
+ border: 1px solid #ccc;
+ padding: 15px;
+ background: #fff;
+ box-shadow: 0px 5px 20px rgba(0,0,0,.2);
+ text-align: left;
+}
+
+div.sharedaddy.sharedaddy-dark #sharing_email {
+ border-color: #fff;
+}
+
+#sharing_email .errors {
+ color: #fff;
+ background-color: #771a09;
+ font-size: 12px;
+ padding: 5px 8px;
+ line-height: 1;
+ margin: 10px 0 0 0;
+}
+
+#sharing_email label {
+ font-size: 12px;
+ color: #333;
+ font-weight: bold;
+ display: block;
+ padding: 0 0 4px 0;
+ text-align: left;
+ text-shadow: none;
+}
+
+#sharing_email form {
+ margin: 0;
+}
+
+#sharing_email input[type="text"], #sharing_email input[type="email"] {
+ width: 100%;
+ box-sizing: border-box;
+ -moz-box-sizing:border-box;
+ -webkit-box-sizing:border-box;
+ border: 1px solid #ccc;
+ margin-bottom: 1em;
+ background: #fff;
+ font-size: 12px;
+ color: #333;
+ max-width: none;
+ padding: 1px 3px;
+}
+#jetpack-source_f_name {
+ display: none!important;
+ position: absolute !important;
+ left: -9000px;
+}
+
+#sharing_email .sharing_cancel {
+ padding: 0 0 0 1em;
+ font-size: 12px;
+ text-shadow: none;
+}
+
+#sharing_email .recaptcha {
+ width: 312px;
+ height: 123px;
+ margin: 0 0 1em 0;
+}
diff --git a/plugins/jetpack/modules/sharedaddy/sharing.js b/plugins/jetpack/modules/sharedaddy/sharing.js
new file mode 100644
index 00000000..dcaf0f11
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/sharing.js
@@ -0,0 +1,514 @@
+/* global WPCOM_sharing_counts, grecaptcha */
+/* jshint unused:false */
+var sharing_js_options;
+if ( sharing_js_options && sharing_js_options.counts ) {
+ var WPCOMSharing = {
+ done_urls: [],
+ get_counts: function() {
+ var url, requests, id, service, service_request;
+
+ if ( 'undefined' === typeof WPCOM_sharing_counts ) {
+ return;
+ }
+
+ for ( url in WPCOM_sharing_counts ) {
+ id = WPCOM_sharing_counts[ url ];
+
+ if ( 'undefined' !== typeof WPCOMSharing.done_urls[ id ] ) {
+ continue;
+ }
+
+ requests = {
+ // Pinterest handles share counts for both http and https
+ pinterest: [
+ window.location.protocol +
+ '//api.pinterest.com/v1/urls/count.json?callback=WPCOMSharing.update_pinterest_count&url=' +
+ encodeURIComponent( url ),
+ ],
+ // Facebook protocol summing has been shown to falsely double counts, so we only request the current URL
+ facebook: [
+ window.location.protocol +
+ '//graph.facebook.com/?callback=WPCOMSharing.update_facebook_count&ids=' +
+ encodeURIComponent( url ),
+ ],
+ };
+
+ for ( service in requests ) {
+ if ( ! jQuery( 'a[data-shared=sharing-' + service + '-' + id + ']' ).length ) {
+ continue;
+ }
+
+ while ( ( service_request = requests[ service ].pop() ) ) {
+ jQuery.getScript( service_request );
+ }
+
+ if ( sharing_js_options.is_stats_active ) {
+ WPCOMSharing.bump_sharing_count_stat( service );
+ }
+ }
+
+ WPCOMSharing.done_urls[ id ] = true;
+ }
+ },
+
+ // get the version of the url that was stored in the dom (sharing-$service-URL)
+ get_permalink: function( url ) {
+ if ( 'https:' === window.location.protocol ) {
+ url = url.replace( /^http:\/\//i, 'https://' );
+ } else {
+ url = url.replace( /^https:\/\//i, 'http://' );
+ }
+
+ return url;
+ },
+ update_facebook_count: function( data ) {
+ var url, permalink;
+
+ if ( ! data ) {
+ return;
+ }
+
+ for ( url in data ) {
+ if (
+ ! data.hasOwnProperty( url ) ||
+ ! data[ url ].share ||
+ ! data[ url ].share.share_count
+ ) {
+ continue;
+ }
+
+ permalink = WPCOMSharing.get_permalink( url );
+
+ if ( ! ( permalink in WPCOM_sharing_counts ) ) {
+ continue;
+ }
+
+ WPCOMSharing.inject_share_count(
+ 'sharing-facebook-' + WPCOM_sharing_counts[ permalink ],
+ data[ url ].share.share_count
+ );
+ }
+ },
+ update_pinterest_count: function( data ) {
+ if ( 'undefined' !== typeof data.count && data.count * 1 > 0 ) {
+ WPCOMSharing.inject_share_count(
+ 'sharing-pinterest-' + WPCOM_sharing_counts[ data.url ],
+ data.count
+ );
+ }
+ },
+ inject_share_count: function( id, count ) {
+ var $share = jQuery( 'a[data-shared=' + id + '] > span' );
+ $share.find( '.share-count' ).remove();
+ $share.append(
+ '<span class="share-count">' + WPCOMSharing.format_count( count ) + '</span>'
+ );
+ },
+ format_count: function( count ) {
+ if ( count < 1000 ) {
+ return count;
+ }
+ if ( count >= 1000 && count < 10000 ) {
+ return String( count ).substring( 0, 1 ) + 'K+';
+ }
+ return '10K+';
+ },
+ bump_sharing_count_stat: function( service ) {
+ new Image().src =
+ document.location.protocol +
+ '//pixel.wp.com/g.gif?v=wpcom-no-pv&x_sharing-count-request=' +
+ service +
+ '&r=' +
+ Math.random();
+ },
+ };
+}
+
+( function( $ ) {
+ var $body, $sharing_email;
+
+ $.fn.extend( {
+ share_is_email: function() {
+ return /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(
+ this.val()
+ );
+ },
+ } );
+
+ $body = $( document.body ).on( 'post-load', WPCOMSharing_do );
+ $( document ).ready( function() {
+ $sharing_email = $( '#sharing_email' );
+ $body.append( $sharing_email );
+ WPCOMSharing_do();
+ } );
+
+ function WPCOMSharing_do() {
+ var $more_sharing_buttons;
+ if ( 'undefined' !== typeof WPCOMSharing ) {
+ WPCOMSharing.get_counts();
+ }
+ $more_sharing_buttons = $( '.sharedaddy a.sharing-anchor' );
+
+ $more_sharing_buttons.click( function() {
+ return false;
+ } );
+
+ $( '.sharedaddy a' ).each( function() {
+ if (
+ $( this ).attr( 'href' ) &&
+ $( this )
+ .attr( 'href' )
+ .indexOf( 'share=' ) !== -1
+ ) {
+ $( this ).attr( 'href', $( this ).attr( 'href' ) + '&nb=1' );
+ }
+ } );
+
+ // Show hidden buttons
+
+ // Touchscreen device: use click.
+ // Non-touchscreen device: use click if not already appearing due to a hover event
+ $more_sharing_buttons.on( 'click', function() {
+ var $more_sharing_button = $( this ),
+ $more_sharing_pane = $more_sharing_button.parents( 'div:first' ).find( '.inner' );
+
+ if ( $more_sharing_pane.is( ':animated' ) ) {
+ // We're in the middle of some other event's animation
+ return;
+ }
+
+ if ( true === $more_sharing_pane.data( 'justSlid' ) ) {
+ // We just finished some other event's animation - don't process click event so that slow-to-react-clickers don't get confused
+ return;
+ }
+
+ $sharing_email.slideUp( 200 );
+
+ $more_sharing_pane
+ .css( {
+ left: $more_sharing_button.position().left + 'px',
+ top: $more_sharing_button.position().top + $more_sharing_button.height() + 3 + 'px',
+ } )
+ .slideToggle( 200 );
+ } );
+
+ if ( document.ontouchstart === undefined ) {
+ // Non-touchscreen device: use hover/mouseout with delay
+ $more_sharing_buttons.hover(
+ function() {
+ var $more_sharing_button = $( this ),
+ $more_sharing_pane = $more_sharing_button.parents( 'div:first' ).find( '.inner' ),
+ timer;
+
+ if ( ! $more_sharing_pane.is( ':animated' ) ) {
+ // Create a timer to make the area appear if the mouse hovers for a period
+ timer = setTimeout( function() {
+ var handler_item_leave,
+ handler_item_enter,
+ handler_original_leave,
+ handler_original_enter,
+ close_it;
+
+ $sharing_email.slideUp( 200 );
+
+ $more_sharing_pane.data( 'justSlid', true );
+ $more_sharing_pane
+ .css( {
+ left: $more_sharing_button.position().left + 'px',
+ top:
+ $more_sharing_button.position().top + $more_sharing_button.height() + 3 + 'px',
+ } )
+ .slideDown( 200, function() {
+ // Mark the item as have being appeared by the hover
+ $more_sharing_button.data( 'hasoriginal', true ).data( 'hasitem', false );
+
+ setTimeout( function() {
+ $more_sharing_pane.data( 'justSlid', false );
+ }, 300 );
+
+ $more_sharing_pane
+ .mouseleave( handler_item_leave )
+ .mouseenter( handler_item_enter );
+ $more_sharing_button
+ .mouseleave( handler_original_leave )
+ .mouseenter( handler_original_enter );
+ } );
+
+ // The following handlers take care of the mouseenter/mouseleave for the share button and the share area - if both are left then we close the share area
+ handler_item_leave = function() {
+ $more_sharing_button.data( 'hasitem', false );
+
+ if ( $more_sharing_button.data( 'hasoriginal' ) === false ) {
+ var timer = setTimeout( close_it, 800 );
+ $more_sharing_button.data( 'timer2', timer );
+ }
+ };
+
+ handler_item_enter = function() {
+ $more_sharing_button.data( 'hasitem', true );
+ clearTimeout( $more_sharing_button.data( 'timer2' ) );
+ };
+
+ handler_original_leave = function() {
+ $more_sharing_button.data( 'hasoriginal', false );
+
+ if ( $more_sharing_button.data( 'hasitem' ) === false ) {
+ var timer = setTimeout( close_it, 800 );
+ $more_sharing_button.data( 'timer2', timer );
+ }
+ };
+
+ handler_original_enter = function() {
+ $more_sharing_button.data( 'hasoriginal', true );
+ clearTimeout( $more_sharing_button.data( 'timer2' ) );
+ };
+
+ close_it = function() {
+ $more_sharing_pane.data( 'justSlid', true );
+ $more_sharing_pane.slideUp( 200, function() {
+ setTimeout( function() {
+ $more_sharing_pane.data( 'justSlid', false );
+ }, 300 );
+ } );
+
+ // Clear all hooks
+ $more_sharing_button
+ .unbind( 'mouseleave', handler_original_leave )
+ .unbind( 'mouseenter', handler_original_enter );
+ $more_sharing_pane
+ .unbind( 'mouseleave', handler_item_leave )
+ .unbind( 'mouseenter', handler_item_leave );
+ return false;
+ };
+ }, 200 );
+
+ // Remember the timer so we can detect it on the mouseout
+ $more_sharing_button.data( 'timer', timer );
+ }
+ },
+ function() {
+ // Mouse out - remove any timer
+ $more_sharing_buttons.each( function() {
+ clearTimeout( $( this ).data( 'timer' ) );
+ } );
+ $more_sharing_buttons.data( 'timer', false );
+ }
+ );
+ } else {
+ $( document.body ).addClass( 'jp-sharing-input-touch' );
+ }
+
+ $( document ).click( function() {
+ // Click outside
+ // remove any timer
+ $more_sharing_buttons.each( function() {
+ clearTimeout( $( this ).data( 'timer' ) );
+ } );
+ $more_sharing_buttons.data( 'timer', false );
+
+ // slide down forcibly
+ $( '.sharedaddy .inner' ).slideUp();
+ } );
+
+ // Add click functionality
+ $( '.sharedaddy ul' ).each( function() {
+ if ( 'yep' === $( this ).data( 'has-click-events' ) ) {
+ return;
+ }
+ $( this ).data( 'has-click-events', 'yep' );
+
+ var printUrl = function( uniqueId, urlToPrint ) {
+ $( 'body:first' ).append(
+ '<iframe style="position:fixed;top:100;left:100;height:1px;width:1px;border:none;" id="printFrame-' +
+ uniqueId +
+ '" name="printFrame-' +
+ uniqueId +
+ '" src="' +
+ urlToPrint +
+ '" onload="frames[\'printFrame-' +
+ uniqueId +
+ "'].focus();frames['printFrame-" +
+ uniqueId +
+ '\'].print();"></iframe>'
+ );
+ };
+
+ // Print button
+ $( this )
+ .find( 'a.share-print' )
+ .click( function() {
+ var ref = $( this ).attr( 'href' ),
+ do_print = function() {
+ if ( ref.indexOf( '#print' ) === -1 ) {
+ var uid = new Date().getTime();
+ printUrl( uid, ref );
+ } else {
+ print();
+ }
+ };
+
+ // Is the button in a dropdown?
+ if ( $( this ).parents( '.sharing-hidden' ).length > 0 ) {
+ $( this )
+ .parents( '.inner' )
+ .slideUp( 0, function() {
+ do_print();
+ } );
+ } else {
+ do_print();
+ }
+
+ return false;
+ } );
+
+ // Press This button
+ $( this )
+ .find( 'a.share-press-this' )
+ .click( function() {
+ var s = '';
+
+ if ( window.getSelection ) {
+ s = window.getSelection();
+ } else if ( document.getSelection ) {
+ s = document.getSelection();
+ } else if ( document.selection ) {
+ s = document.selection.createRange().text;
+ }
+
+ if ( s ) {
+ $( this ).attr( 'href', $( this ).attr( 'href' ) + '&sel=' + encodeURI( s ) );
+ }
+
+ if (
+ ! window.open(
+ $( this ).attr( 'href' ),
+ 't',
+ 'toolbar=0,resizable=1,scrollbars=1,status=1,width=720,height=570'
+ )
+ ) {
+ document.location.href = $( this ).attr( 'href' );
+ }
+
+ return false;
+ } );
+
+ // Email button
+ $( 'a.share-email', this ).on( 'click', function() {
+ var url = $( this ).attr( 'href' );
+ var currentDomain = window.location.protocol + '//' + window.location.hostname + '/';
+ if ( url.indexOf( currentDomain ) !== 0 ) {
+ return true;
+ }
+
+ if ( $sharing_email.is( ':visible' ) ) {
+ $sharing_email.slideUp( 200 );
+ } else {
+ $( '.sharedaddy .inner' ).slideUp();
+
+ $( '#sharing_email .response' ).remove();
+ $( '#sharing_email form' ).show();
+ $( '#sharing_email form input[type=submit]' ).removeAttr( 'disabled' );
+ $( '#sharing_email form a.sharing_cancel' ).show();
+
+ // Reset reCATPCHA if exists.
+ if (
+ 'object' === typeof grecaptcha &&
+ 'function' === typeof grecaptcha.reset &&
+ window.___grecaptcha_cfg.count
+ ) {
+ grecaptcha.reset();
+ }
+
+ // Show dialog
+ $sharing_email
+ .css( {
+ left: $( this ).offset().left + 'px',
+ top: $( this ).offset().top + $( this ).height() + 'px',
+ } )
+ .slideDown( 200 );
+
+ // Hook up other buttons
+ $( '#sharing_email a.sharing_cancel' )
+ .unbind( 'click' )
+ .click( function() {
+ $( '#sharing_email .errors' ).hide();
+ $sharing_email.slideUp( 200 );
+ $( '#sharing_background' ).fadeOut();
+ return false;
+ } );
+
+ // Submit validation
+ $( '#sharing_email input[type=submit]' )
+ .unbind( 'click' )
+ .click( function() {
+ var form = $( this ).parents( 'form' );
+ var source_email_input = form.find( 'input[name=source_email]' );
+ var target_email_input = form.find( 'input[name=target_email]' );
+
+ // Disable buttons + enable loading icon
+ $( this ).prop( 'disabled', true );
+ form.find( 'a.sharing_cancel' ).hide();
+ form.find( 'img.loading' ).show();
+
+ $( '#sharing_email .errors' ).hide();
+ $( '#sharing_email .error' ).removeClass( 'error' );
+
+ if ( ! source_email_input.share_is_email() ) {
+ source_email_input.addClass( 'error' );
+ }
+
+ if ( ! target_email_input.share_is_email() ) {
+ target_email_input.addClass( 'error' );
+ }
+
+ if ( $( '#sharing_email .error' ).length === 0 ) {
+ // AJAX send the form
+ $.ajax( {
+ url: url,
+ type: 'POST',
+ data: form.serialize(),
+ success: function( response ) {
+ form.find( 'img.loading' ).hide();
+
+ if ( response === '1' || response === '2' || response === '3' ) {
+ $( '#sharing_email .errors-' + response ).show();
+ form.find( 'input[type=submit]' ).removeAttr( 'disabled' );
+ form.find( 'a.sharing_cancel' ).show();
+
+ if (
+ 'object' === typeof grecaptcha &&
+ 'function' === typeof grecaptcha.reset
+ ) {
+ grecaptcha.reset();
+ }
+ } else {
+ $( '#sharing_email form' ).hide();
+ $sharing_email.append( response );
+ $( '#sharing_email a.sharing_cancel' ).click( function() {
+ $sharing_email.slideUp( 200 );
+ $( '#sharing_background' ).fadeOut();
+ return false;
+ } );
+ }
+ },
+ } );
+
+ return false;
+ }
+
+ form.find( 'img.loading' ).hide();
+ form.find( 'input[type=submit]' ).removeAttr( 'disabled' );
+ form.find( 'a.sharing_cancel' ).show();
+ $( '#sharing_email .errors-1' ).show();
+
+ return false;
+ } );
+ }
+
+ return false;
+ } );
+ } );
+
+ $( 'li.share-email, li.share-custom a.sharing-anchor' ).addClass( 'share-service-visible' );
+ }
+} )( jQuery );
diff --git a/plugins/jetpack/modules/sharedaddy/sharing.php b/plugins/jetpack/modules/sharedaddy/sharing.php
new file mode 100644
index 00000000..49df892b
--- /dev/null
+++ b/plugins/jetpack/modules/sharedaddy/sharing.php
@@ -0,0 +1,638 @@
+<?php
+if ( ! defined( 'WP_SHARING_PLUGIN_URL' ) ) {
+ define( 'WP_SHARING_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
+ define( 'WP_SHARING_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
+}
+
+class Sharing_Admin {
+ public function __construct() {
+ require_once WP_SHARING_PLUGIN_DIR . 'sharing-service.php';
+
+ add_action( 'admin_init', array( &$this, 'admin_init' ) );
+ add_action( 'admin_menu', array( &$this, 'subscription_menu' ) );
+
+ // Insert our CSS and JS
+ add_action( 'load-settings_page_sharing', array( &$this, 'sharing_head' ) );
+
+ // Catch AJAX
+ add_action( 'wp_ajax_sharing_save_services', array( &$this, 'ajax_save_services' ) );
+ add_action( 'wp_ajax_sharing_save_options', array( &$this, 'ajax_save_options' ) );
+ add_action( 'wp_ajax_sharing_new_service', array( &$this, 'ajax_new_service' ) );
+ add_action( 'wp_ajax_sharing_delete_service', array( &$this, 'ajax_delete_service' ) );
+ }
+
+ public function sharing_head() {
+ wp_enqueue_script(
+ 'sharing-js',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/sharedaddy/admin-sharing.min.js',
+ 'modules/sharedaddy/admin-sharing.js'
+ ),
+ array( 'jquery-ui-draggable', 'jquery-ui-droppable', 'jquery-ui-sortable', 'jquery-form' ),
+ 2
+ );
+
+ /**
+ * Filters the switch that if set to true allows Jetpack to use minified assets. Defaults to true
+ * if the SCRIPT_DEBUG constant is not set or set to false. The filter overrides it.
+ *
+ * @since 6.2.0
+ *
+ * @param boolean $var should Jetpack use minified assets.
+ */
+ $postfix = apply_filters( 'jetpack_should_use_minified_assets', true ) ? '.min' : '';
+ if ( is_rtl() ) {
+ wp_enqueue_style( 'sharing-admin', WP_SHARING_PLUGIN_URL . 'admin-sharing-rtl' . $postfix . '.css', false, JETPACK__VERSION );
+ } else {
+ wp_enqueue_style( 'sharing-admin', WP_SHARING_PLUGIN_URL . 'admin-sharing' . $postfix . '.css', false, JETPACK__VERSION );
+ }
+ wp_enqueue_style( 'sharing', WP_SHARING_PLUGIN_URL . 'sharing.css', false, JETPACK__VERSION );
+
+ wp_enqueue_style( 'social-logos' );
+ wp_enqueue_script( 'sharing-js-fe', WP_SHARING_PLUGIN_URL . 'sharing.js', array(), 4 );
+ add_thickbox();
+
+ // On Jetpack sites, make sure we include CSS to style the admin page.
+ if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
+ Jetpack_Admin_Page::load_wrapper_styles();
+ }
+ }
+
+ public function admin_init() {
+ if ( isset( $_GET['page'] ) && ( $_GET['page'] == 'sharing.php' || $_GET['page'] == 'sharing' ) ) {
+ $this->process_requests();
+ }
+ }
+
+ public function process_requests() {
+ if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'sharing-options' ) ) {
+ $sharer = new Sharing_Service();
+ $sharer->set_global_options( $_POST );
+ /**
+ * Fires when updating sharing settings.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ */
+ do_action( 'sharing_admin_update' );
+
+ wp_safe_redirect( admin_url( 'options-general.php?page=sharing&update=saved' ) );
+ die();
+ }
+ }
+
+ public function subscription_menu( $user ) {
+ if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
+ $active = Jetpack::get_active_modules();
+ if ( ! in_array( 'publicize', $active ) && ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+ }
+
+ add_submenu_page(
+ 'options-general.php',
+ __( 'Sharing Settings', 'jetpack' ),
+ __( 'Sharing', 'jetpack' ),
+ 'publish_posts',
+ 'sharing',
+ array( &$this, 'wrapper_admin_page' )
+ );
+ }
+
+ public function ajax_save_services() {
+ if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'sharing-options' ) && isset( $_POST['hidden'] ) && isset( $_POST['visible'] ) ) {
+ $sharer = new Sharing_Service();
+
+ $sharer->set_blog_services( explode( ',', $_POST['visible'] ), explode( ',', $_POST['hidden'] ) );
+ die();
+ }
+ }
+
+ public function ajax_new_service() {
+ if ( isset( $_POST['_wpnonce'] ) && isset( $_POST['sharing_name'] ) && isset( $_POST['sharing_url'] ) && isset( $_POST['sharing_icon'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'sharing-new_service' ) ) {
+ $sharer = new Sharing_Service();
+ if ( $service = $sharer->new_service( stripslashes( $_POST['sharing_name'] ), stripslashes( $_POST['sharing_url'] ), stripslashes( $_POST['sharing_icon'] ) ) ) {
+ $this->output_service( $service->get_id(), $service );
+ echo '<!--->';
+ $service->button_style = 'icon-text';
+ $this->output_preview( $service );
+
+ die();
+ }
+ }
+
+ // Fail
+ die( '1' );
+ }
+
+ public function ajax_delete_service() {
+ if ( isset( $_POST['_wpnonce'] ) && isset( $_POST['service'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'sharing-options_' . $_POST['service'] ) ) {
+ $sharer = new Sharing_Service();
+ $sharer->delete_service( $_POST['service'] );
+ }
+ }
+
+ public function ajax_save_options() {
+ if ( isset( $_POST['_wpnonce'] ) && isset( $_POST['service'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'sharing-options_' . $_POST['service'] ) ) {
+ $sharer = new Sharing_Service();
+ $service = $sharer->get_service( $_POST['service'] );
+
+ if ( $service && $service instanceof Sharing_Advanced_Source ) {
+ $service->update_options( $_POST );
+
+ $sharer->set_service( $_POST['service'], $service );
+ }
+
+ $this->output_service( $service->get_id(), $service, true );
+ echo '<!--->';
+ $service->button_style = 'icon-text';
+ $this->output_preview( $service );
+ die();
+ }
+ }
+
+ public function output_preview( $service ) {
+
+ $klasses = array( 'advanced', 'preview-item' );
+
+ if ( $service->button_style != 'text' || $service->has_custom_button_style() ) {
+ $klasses[] = 'preview-' . $service->get_class();
+ $klasses[] = 'share-' . $service->get_class();
+ if ( $service->is_deprecated() ) {
+ $klasses[] = 'share-deprecated';
+ }
+
+ if ( $service->get_class() != $service->get_id() ) {
+ $klasses[] = 'preview-' . $service->get_id();
+ }
+ }
+
+ echo '<li class="' . implode( ' ', $klasses ) . '">';
+ $service->display_preview();
+ echo '</li>';
+ }
+
+ public function output_service( $id, $service, $show_dropdown = false ) {
+ $title = '';
+ $klasses = array( 'service', 'advanced', 'share-' . $service->get_class() );
+ if ( $service->is_deprecated() ) {
+ $title = sprintf( __( 'The %1$s service has shut down. This sharing button is not displayed to your visitors and should be removed.', 'jetpack' ), $service->get_name() );
+ $klasses[] = 'share-deprecated';
+ }
+
+?>
+ <li class="<?php echo implode( ' ', $klasses ); ?>" id="<?php echo $service->get_id(); ?>" tabindex="0" title="<?php echo esc_attr( $title ); ?>">
+ <span class="options-left"><?php echo esc_html( $service->get_name() ); ?></span>
+ <?php if ( 0 === strpos( $service->get_id(), 'custom-' ) || $service->has_advanced_options() ) : ?>
+ <span class="close"><a href="#" class="remove">&times;</a></span>
+ <form method="post" action="<?php echo admin_url( 'admin-ajax.php' ); ?>">
+ <input type="hidden" name="action" value="sharing_delete_service" />
+ <input type="hidden" name="service" value="<?php echo esc_attr( $id ); ?>" />
+ <input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce( 'sharing-options_' . $id );?>" />
+ </form>
+ <?php endif; ?>
+ </li>
+<?php
+ }
+
+ public function wrapper_admin_page() {
+ Jetpack_Admin_Page::wrap_ui( array( &$this, 'management_page' ), array( 'is-wide' =>true ) );
+ }
+
+ public function management_page() {
+ $sharer = new Sharing_Service();
+ $enabled = $sharer->get_blog_services();
+ $global = $sharer->get_global_options();
+
+ $shows = array_values( get_post_types( array( 'public' => true ) ) );
+ array_unshift( $shows, 'index' );
+
+ if ( false == function_exists( 'mb_stripos' ) ) {
+ echo '<div id="message" class="updated fade"><h3>' . __( 'Warning! Multibyte support missing!', 'jetpack' ) . '</h3>';
+ echo '<p>' . sprintf( __( 'This plugin will work without it, but multibyte support is used <a href="%s" rel="noopener noreferrer" target="_blank">if available</a>. You may see minor problems with Tweets and other sharing services.', 'jetpack' ), 'http://www.php.net/manual/en/mbstring.installation.php' ) . '</p></div>';
+ }
+
+ if ( isset( $_GET['update'] ) && $_GET['update'] == 'saved' ) {
+ echo '<div class="updated"><p>' . __( 'Settings have been saved', 'jetpack' ) . '</p></div>';
+ }
+
+ if ( ! isset( $global['sharing_label'] ) ) {
+ $global['sharing_label'] = __( 'Share this:', 'jetpack' );
+ }
+?>
+
+ <div class="wrap">
+ <div class="icon32" id="icon-options-general"><br /></div>
+ <h1><?php _e( 'Sharing Settings', 'jetpack' ); ?></h1>
+
+ <?php
+ /**
+ * Fires at the top of the admin sharing settings screen.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.6.0
+ */
+ do_action( 'pre_admin_screen_sharing' );
+ ?>
+
+ <?php if ( current_user_can( 'manage_options' ) ) : ?>
+
+ <div class="share_manage_options">
+ <h2><?php _e( 'Sharing Buttons', 'jetpack' ) ?></h2>
+ <p><?php _e( 'Add sharing buttons to your blog and allow your visitors to share posts with their friends.', 'jetpack' ) ?></p>
+
+ <div id="services-config">
+ <table id="available-services">
+ <tr>
+ <td class="description">
+ <h3><?php _e( 'Available Services', 'jetpack' ); ?></h3>
+ <p><?php _e( "Drag and drop the services you'd like to enable into the box below.", 'jetpack' ); ?></p>
+ <p><a href="#TB_inline?height=395&amp;width=600&amp;inlineId=new-service" class="thickbox" id="add-a-new-service"><?php _e( 'Add a new service', 'jetpack' ); ?></a></p>
+ </td>
+ <td class="services">
+ <ul class="services-available" style="height: 100px;">
+ <?php foreach ( $sharer->get_all_services_blog() as $id => $service ) : ?>
+ <?php
+ if ( ! isset( $enabled['all'][ $id ] ) ) {
+ $this->output_service( $id, $service );
+ }
+ ?>
+ <?php endforeach; ?>
+ </ul>
+ <?php
+ if ( -1 == get_option( 'blog_public' ) ) {
+ echo '<p><strong>' . __( 'Please note that your services have been restricted because your site is private.', 'jetpack' ) . '</strong></p>';
+ }
+ ?>
+ <br class="clearing" />
+ </td>
+ </tr>
+ </table>
+
+ <table id="enabled-services">
+ <tr>
+ <td class="description">
+ <h3>
+ <?php _e( 'Enabled Services', 'jetpack' ); ?>
+ <img src="<?php echo admin_url( 'images/loading.gif' ); ?>" width="16" height="16" alt="loading" style="vertical-align: middle; display: none" />
+ </h3>
+ <p><?php _e( 'Services dragged here will appear individually.', 'jetpack' ); ?></p>
+ </td>
+ <td class="services" id="share-drop-target">
+ <h2 id="drag-instructions" <?php if ( count( $enabled['visible'] ) > 0 ) { echo ' style="display: none"';} ?>><?php _e( 'Drag and drop available services here.', 'jetpack' ); ?></h2>
+
+ <ul class="services-enabled">
+ <?php foreach ( $enabled['visible'] as $id => $service ) : ?>
+ <?php $this->output_service( $id, $service, true ); ?>
+ <?php endforeach; ?>
+
+ <li class="end-fix"></li>
+ </ul>
+ </td>
+ <td id="hidden-drop-target" class="services">
+ <p><?php _e( 'Services dragged here will be hidden behind a share button.', 'jetpack' ); ?></p>
+
+ <ul class="services-hidden">
+ <?php foreach ( $enabled['hidden'] as $id => $service ) : ?>
+ <?php $this->output_service( $id, $service, true ); ?>
+ <?php endforeach; ?>
+ <li class="end-fix"></li>
+ </ul>
+ </td>
+ </tr>
+ </table>
+
+ <table id="live-preview">
+ <tr>
+ <td class="description">
+ <h3><?php _e( 'Live Preview', 'jetpack' ); ?></h3>
+ </td>
+ <td class="services">
+ <h2 <?php echo ( count( $enabled['all'] ) > 0 ) ? ' style="display: none"' : ''; ?>><?php _e( 'Sharing is off. Add services above to enable.', 'jetpack' ); ?></h2>
+ <div class="sharedaddy sd-sharing-enabled">
+ <?php if ( count( $enabled['all'] ) > 0 ) : ?>
+ <h3 class="sd-title"><?php echo esc_html( $global['sharing_label'] ); ?></h3>
+ <?php endif; ?>
+ <div class="sd-content">
+ <ul class="preview">
+ <?php foreach ( $enabled['visible'] as $id => $service ) : ?>
+ <?php $this->output_preview( $service ); ?>
+ <?php endforeach; ?>
+
+ <?php if ( count( $enabled['hidden'] ) > 0 ) : ?>
+ <li class="advanced"><a href="#" class="sharing-anchor sd-button share-more"><span><?php _e( 'More', 'jetpack' ); ?></span></a></li>
+ <?php endif; ?>
+ </ul>
+
+ <?php if ( count( $enabled['hidden'] ) > 0 ) : ?>
+ <div class="sharing-hidden">
+ <div class="inner" style="display: none; <?php echo count( $enabled['hidden'] ) == 1 ? 'width:150px;' : ''; ?>">
+ <?php if ( count( $enabled['hidden'] ) == 1 ) : ?>
+ <ul style="background-image:none;">
+ <?php else : ?>
+ <ul>
+ <?php endif; ?>
+
+ <?php
+ foreach ( $enabled['hidden'] as $id => $service ) {
+ $this->output_preview( $service );
+ }
+ ?>
+ </ul>
+ </div>
+ </div>
+ <?php endif; ?>
+
+ <ul class="archive" style="display:none;">
+ <?php
+ foreach ( $sharer->get_all_services_blog() as $id => $service ) :
+ if ( isset( $enabled['visible'][ $id ] ) ) {
+ $service = $enabled['visible'][ $id ];
+ } elseif ( isset( $enabled['hidden'][ $id ] ) ) {
+ $service = $enabled['hidden'][ $id ];
+ }
+
+ $service->button_style = 'icon-text'; // The archive needs the full text, which is removed in JS later
+ $service->smart = false;
+ $this->output_preview( $service );
+ endforeach; ?>
+ <li class="advanced"><a href="#" class="sharing-anchor sd-button share-more"><span><?php _e( 'More', 'jetpack' ); ?></span></a></li>
+ </ul>
+ </div>
+ </div>
+ <br class="clearing" />
+ </td>
+ </tr>
+ </table>
+
+ <form method="post" action="<?php echo admin_url( 'admin-ajax.php' ); ?>" id="save-enabled-shares">
+ <input type="hidden" name="action" value="sharing_save_services" />
+ <input type="hidden" name="visible" value="<?php echo implode( ',', array_keys( $enabled['visible'] ) ); ?>" />
+ <input type="hidden" name="hidden" value="<?php echo implode( ',', array_keys( $enabled['hidden'] ) ); ?>" />
+ <input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce( 'sharing-options' );?>" />
+ </form>
+ </div>
+
+ <form method="post" action="">
+ <table class="form-table">
+ <tbody>
+ <tr valign="top">
+ <th scope="row"><label><?php _e( 'Button style', 'jetpack' ); ?></label></th>
+ <td>
+ <select name="button_style" id="button_style">
+ <option<?php echo ( $global['button_style'] == 'icon-text' ) ? ' selected="selected"' : ''; ?> value="icon-text"><?php _e( 'Icon + text', 'jetpack' ); ?></option>
+ <option<?php echo ( $global['button_style'] == 'icon' ) ? ' selected="selected"' : ''; ?> value="icon"><?php _e( 'Icon only', 'jetpack' ); ?></option>
+ <option<?php echo ( $global['button_style'] == 'text' ) ? ' selected="selected"' : ''; ?> value="text"><?php _e( 'Text only', 'jetpack' ); ?></option>
+ <option<?php echo ( $global['button_style'] == 'official' ) ? ' selected="selected"' : ''; ?> value="official"><?php _e( 'Official buttons', 'jetpack' ); ?></option>
+ </select>
+ </td>
+ </tr>
+ <tr valign="top">
+ <th scope="row"><label><?php _e( 'Sharing label', 'jetpack' ); ?></label></th>
+ <td>
+ <input type="text" name="sharing_label" value="<?php echo esc_attr( $global['sharing_label'] ); ?>" />
+ </td>
+ </tr>
+ <?php
+ /**
+ * Filters the HTML at the beginning of the "Show button on" row.
+ *
+ * @module sharedaddy
+ *
+ * @since 2.1.0
+ *
+ * @param string $var Opening HTML tag at the beginning of the "Show button on" row.
+ */
+ echo apply_filters( 'sharing_show_buttons_on_row_start', '<tr valign="top">' );
+ ?>
+ <th scope="row"><label><?php _e( 'Show buttons on', 'jetpack' ); ?></label></th>
+ <td>
+ <?php
+ $br = false;
+ foreach ( $shows as $show ) :
+ if ( 'index' == $show ) {
+ $label = __( 'Front Page, Archive Pages, and Search Results', 'jetpack' );
+ } else {
+ $post_type_object = get_post_type_object( $show );
+ $label = $post_type_object->labels->name;
+ }
+ ?>
+ <?php
+ if ( $br ) {
+ echo '<br />';
+ }
+ ?>
+ <label><input type="checkbox"<?php checked( in_array( $show, $global['show'] ) ); ?> name="show[]" value="<?php echo esc_attr( $show ); ?>" /> <?php echo esc_html( $label ); ?></label>
+ <?php
+ $br = true;
+ endforeach;
+ ?>
+ </td>
+ <?php
+ /**
+ * Filters the HTML at the end of the "Show button on" row.
+ *
+ * @module sharedaddy
+ *
+ * @since 2.1.0
+ *
+ * @param string $var Closing HTML tag at the end of the "Show button on" row.
+ */
+ echo apply_filters( 'sharing_show_buttons_on_row_end', '</tr>' );
+ ?>
+
+ <?php
+ /**
+ * Fires at the end of the sharing global options settings table.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ */
+ do_action( 'sharing_global_options' );
+ ?>
+ </tbody>
+ </table>
+
+ <p class="submit">
+ <input type="submit" name="submit" class="button-primary" value="<?php esc_attr_e( 'Save Changes', 'jetpack' ); ?>" />
+ </p>
+
+ <input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce( 'sharing-options' );?>" />
+ </form>
+
+ <div id="new-service" style="display: none">
+ <form method="post" action="<?php echo admin_url( 'admin-ajax.php' ); ?>" id="new-service-form">
+ <table class="form-table">
+ <tbody>
+ <tr valign="top">
+ <th scope="row" width="100"><label><?php _e( 'Service name', 'jetpack' ); ?></label></th>
+ <td>
+ <input type="text" name="sharing_name" id="new_sharing_name" size="40" />
+ </td>
+ </tr>
+ <tr valign="top">
+ <th scope="row" width="100"><label><?php _e( 'Sharing URL', 'jetpack' ); ?></label></th>
+ <td>
+ <input type="text" name="sharing_url" id="new_sharing_url" size="40" />
+
+ <p><?php _e( 'You can add the following variables to your service sharing URL:', 'jetpack' ); ?><br/>
+ <code>%post_id%</code>, <code>%post_title%</code>, <code>%post_slug%</code>, <code>%post_url%</code>, <code>%post_full_url%</code>, <code>%post_excerpt%</code>, <code>%post_tags%</code>, <code>%home_url%</code></p>
+ </td>
+ </tr>
+ <tr valign="top">
+ <th scope="row" width="100"><label><?php _e( 'Icon URL', 'jetpack' ); ?></label></th>
+ <td>
+ <input type="text" name="sharing_icon" id="new_sharing_icon" size="40" />
+ <p><?php _e( 'Enter the URL of a 16x16px icon you want to use for this service.', 'jetpack' ); ?></p>
+ </td>
+ </tr>
+ <tr valign="top" width="100">
+ <th scope="row"></th>
+ <td>
+ <input type="submit" class="button-primary" value="<?php esc_attr_e( 'Create Share Button', 'jetpack' ); ?>" />
+ <img src="<?php echo admin_url( 'images/loading.gif' ); ?>" width="16" height="16" alt="loading" style="vertical-align: middle; display: none" />
+ </td>
+ </tr>
+
+ <?php
+ /**
+ * Fires after the custom sharing service form
+ *
+ * @module sharedaddy
+ *
+ * @since 1.1.0
+ */
+ do_action( 'sharing_new_service_form' );
+ ?>
+ </tbody>
+ </table>
+
+ <?php
+ /**
+ * Fires at the bottom of the admin sharing settings screen.
+ *
+ * @module sharedaddy
+ *
+ * @since 1.6.0
+ */
+ do_action( 'post_admin_screen_sharing' );
+ ?>
+
+ <div class="inerror" style="display: none; margin-top: 15px">
+ <p><?php _e( 'An error occurred creating your new sharing service - please check you gave valid details.', 'jetpack' ); ?></p>
+ </div>
+
+ <input type="hidden" name="action" value="sharing_new_service" />
+ <input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce( 'sharing-new_service' );?>" />
+ </form>
+ </div>
+ </div>
+
+ <?php endif; ?>
+
+
+ </div>
+
+ <script type="text/javascript">
+ var sharing_loading_icon = '<?php echo esc_js( admin_url( '/images/loading.gif' ) ); ?>';
+ <?php if ( isset( $_GET['create_new_service'] ) && 'true' == $_GET['create_new_service'] ) : ?>
+ jQuery(document).ready(function() {
+ // Prefill new service box and then open it
+ jQuery( '#new_sharing_name' ).val( '<?php echo esc_js( $_GET['name'] ); ?>' );
+ jQuery( '#new_sharing_url' ).val( '<?php echo esc_js( $_GET['url'] ); ?>' );
+ jQuery( '#new_sharing_icon' ).val( '<?php echo esc_js( $_GET['icon'] ); ?>' );
+ jQuery( '#add-a-new-service' ).click();
+ });
+ <?php endif; ?>
+ </script>
+<?php
+ }
+}
+
+/**
+ * Callback to get the value for the jetpack_sharing_enabled field.
+ *
+ * When the sharing_disabled post_meta is unset, we follow the global setting in Sharing.
+ * When it is set to 1, we disable sharing on the post, regardless of the global setting.
+ * It is not possible to enable sharing on a post if it is disabled globally.
+ */
+function jetpack_post_sharing_get_value( array $post ) {
+ // if sharing IS disabled on this post, enabled=false, so negate the meta
+ return (bool) ! get_post_meta( $post['id'], 'sharing_disabled', true );
+}
+
+/**
+ * Callback to set sharing_disabled post_meta when the
+ * jetpack_sharing_enabled field is updated.
+ *
+ * When the sharing_disabled post_meta is unset, we follow the global setting in Sharing.
+ * When it is set to 1, we disable sharing on the post, regardless of the global setting.
+ * It is not possible to enable sharing on a post if it is disabled globally.
+ *
+ */
+function jetpack_post_sharing_update_value( $enable_sharing, $post_object ) {
+ if ( $enable_sharing ) {
+ // delete the override if we want to enable sharing
+ return delete_post_meta( $post_object->ID, 'sharing_disabled' );
+ } else {
+ return update_post_meta( $post_object->ID, 'sharing_disabled', true );
+ }
+}
+
+/**
+ * Add Sharing post_meta to the REST API Post response.
+ *
+ * @action rest_api_init
+ * @uses register_rest_field
+ * @link https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-responses/
+ */
+function jetpack_post_sharing_register_rest_field() {
+ $post_types = get_post_types( array( 'public' => true ) );
+ foreach ( $post_types as $post_type ) {
+ register_rest_field(
+ $post_type,
+ 'jetpack_sharing_enabled',
+ array(
+ 'get_callback' => 'jetpack_post_sharing_get_value',
+ 'update_callback' => 'jetpack_post_sharing_update_value',
+ 'schema' => array(
+ 'description' => __( 'Are sharing buttons enabled?', 'jetpack' ),
+ 'type' => 'boolean',
+ ),
+ )
+ );
+
+ /**
+ * Ensures all public internal post-types support `sharing`
+ * This feature support flag is used by the REST API and Gutenberg.
+ */
+ add_post_type_support( $post_type, 'jetpack-sharing-buttons' );
+ }
+}
+
+// Add Sharing post_meta to the REST API Post response.
+add_action( 'rest_api_init', 'jetpack_post_sharing_register_rest_field' );
+
+// Some CPTs (e.g. Jetpack portfolios and testimonials) get registered with
+// restapi_theme_init because they depend on theme support, so let's also hook to that
+add_action( 'restapi_theme_init', 'jetpack_post_likes_register_rest_field', 20 );
+
+function sharing_admin_init() {
+ global $sharing_admin;
+
+ $sharing_admin = new Sharing_Admin();
+}
+
+/**
+ * Set the Likes and Sharing Gutenberg extension as available
+ */
+function jetpack_sharing_set_extension_availability() {
+ Jetpack_Gutenberg::set_extension_available( 'sharing' );
+}
+
+add_action( 'jetpack_register_gutenberg_extensions', 'jetpack_sharing_set_extension_availability' );
+
+add_action( 'init', 'sharing_admin_init' );
diff --git a/plugins/jetpack/modules/shortcodes.php b/plugins/jetpack/modules/shortcodes.php
new file mode 100644
index 00000000..23ed63e1
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes.php
@@ -0,0 +1,196 @@
+<?php
+
+/**
+ * Module Name: Shortcode Embeds
+ * Module Description: Shortcodes are WordPress-specific markup that let you add media from popular sites. This feature is no longer necessary as the editor now handles media embeds rather gracefully.
+ * Sort Order: 3
+ * First Introduced: 1.1
+ * Major Changes In: 1.2
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Photos and Videos, Social, Writing, Appearance
+ * Feature: Writing
+ * Additional Search Queries: shortcodes, shortcode, embeds, media, bandcamp, dailymotion, facebook, flickr, google calendars, google maps, google+, polldaddy, recipe, recipes, scribd, slideshare, slideshow, slideshows, soundcloud, ted, twitter, vimeo, vine, youtube
+ */
+
+/**
+ * Transforms the $atts array into a string that the old functions expected
+ *
+ * The old way was:
+ * [shortcode a=1&b=2&c=3] or [shortcode=1]
+ * This is parsed as array( a => '1&b=2&c=3' ) and array( 0 => '=1' ), which is useless
+ *
+ * @param array $params Array of old shortcode parameters.
+ * @param bool $old_format_support true if [shortcode=foo] format is possible.
+ *
+ * @return string $params
+ */
+function shortcode_new_to_old_params( $params, $old_format_support = false ) {
+ $str = '';
+
+ if ( $old_format_support && isset( $params[0] ) ) {
+ $str = ltrim( $params[0], '=' );
+ } elseif ( is_array( $params ) ) {
+ foreach ( array_keys( $params ) as $key ) {
+ if ( ! is_numeric( $key ) ) {
+ $str = $key . '=' . $params[ $key ];
+ }
+ }
+ }
+
+ return str_replace( array( '&amp;', '&#038;' ), '&', $str );
+}
+
+/**
+ * Load all available Jetpack shortcode files.
+ */
+function jetpack_load_shortcodes() {
+ $shortcode_includes = array();
+
+ foreach ( Jetpack::glob_php( dirname( __FILE__ ) . '/shortcodes' ) as $file ) {
+ $filename = substr( basename( $file ), 0, -4 );
+
+ $shortcode_includes[ $filename ] = $file;
+ }
+
+ /**
+ * This filter allows other plugins to override which shortcodes Jetpack loads.
+ *
+ * Fires as part of the `plugins_loaded` WP hook, so modifying code needs to be in a plugin, not in a theme's functions.php.
+ *
+ * @module shortcodes
+ *
+ * @since 2.2.1
+ * @since 4.2.0 Added filename without extension as array key.
+ *
+ * @param array $shortcode_includes An array of which shortcodes to include.
+ */
+ $shortcode_includes = apply_filters( 'jetpack_shortcodes_to_include', $shortcode_includes );
+
+ foreach ( $shortcode_includes as $include ) {
+ include_once $include;
+ }
+}
+
+/**
+ * Runs preg_replace so that replacements don't happen within open tags.
+ * Parameters are the same as preg_replace, with an added optional search param for improved performance
+ *
+ * @param string $pattern Pattern to search for.
+ * @param string $replacement String to replace.
+ * @param string $content Post content.
+ * @param string $search String to search for.
+ *
+ * @return string $content Replaced post content.
+ */
+function jetpack_preg_replace_outside_tags( $pattern, $replacement, $content, $search = null ) {
+ if ( $search && false === strpos( $content, $search ) ) {
+ return $content;
+ }
+
+ $textarr = wp_html_split( $content );
+ unset( $content );
+ foreach ( $textarr as &$element ) {
+ if ( '' === $element || '<' === $element{0} ) {
+ continue;
+ }
+ $element = preg_replace( $pattern, $replacement, $element );
+ }
+
+ return join( $textarr );
+}
+
+/**
+ * Runs preg_replace_callback so that replacements don't happen within open tags.
+ * Parameters are the same as preg_replace, with an added optional search param for improved performance.
+ *
+ * @param string $pattern Pattern to search for.
+ * @param string $callback Callback returning the replacement string.
+ * @param string $content Post content.
+ * @param string $search String to search for.
+ *
+ * @return string $content Replaced post content.
+ */
+function jetpack_preg_replace_callback_outside_tags( $pattern, $callback, $content, $search = null ) {
+ if ( $search && false === strpos( $content, $search ) ) {
+ return $content;
+ }
+
+ $textarr = wp_html_split( $content );
+ unset( $content );
+ foreach ( $textarr as &$element ) {
+ if ( '' === $element || '<' === $element{0} ) {
+ continue;
+ }
+ $element = preg_replace_callback( $pattern, $callback, $element );
+ }
+
+ return join( $textarr );
+}
+
+if ( ! function_exists( 'jetpack_shortcode_get_wpvideo_id' ) ) {
+ /**
+ * Get VideoPress ID from wpvideo shortcode attributes.
+ *
+ * @param array $atts Shortcode attributes.
+ * @return int $id VideoPress ID.
+ */
+ function jetpack_shortcode_get_wpvideo_id( $atts ) {
+ if ( isset( $atts[0] ) ) {
+ return $atts[0];
+ } else {
+ return 0;
+ }
+ }
+}
+
+if ( ! function_exists( 'jetpack_shortcode_get_videopress_id' ) ) {
+ /**
+ * Get VideoPress ID from videopress shortcode attributes.
+ *
+ * @param array $atts Shortcode attributes.
+ * @return int $id VideoPress ID.
+ */
+ function jetpack_shortcode_get_videopress_id( $atts ) {
+ if ( isset( $atts[0] ) ) {
+ return $atts[0];
+ } else {
+ return 0;
+ }
+ }
+}
+
+/**
+ * Common element attributes parsing and sanitizing for src, width and height.
+ *
+ * @since 4.5.0
+ *
+ * @param array $attrs With original values.
+ *
+ * @return array $attrs With sanitized values.
+ */
+function wpcom_shortcodereverse_parseattr( $attrs ) {
+ $defaults = array(
+ 'src' => false,
+ 'width' => false,
+ 'height' => false,
+ );
+
+ $attrs = shortcode_atts( $defaults, $attrs );
+
+ $attrs['src'] = strip_tags( $attrs['src'] ); // For sanity
+ $attrs['width'] = ( is_numeric( $attrs['width'] ) ) ? abs( intval( $attrs['width'] ) ) : $defaults['width'];
+ $attrs['height'] = ( is_numeric( $attrs['height'] ) ) ? abs( intval( $attrs['height'] ) ) : $defaults['height'];
+
+ return $attrs;
+}
+
+/**
+ * When an embed service goes away, we can use this handler
+ * to output a link for history's sake.
+ */
+function jetpack_deprecated_embed_handler( $matches, $attr, $url ) {
+ return sprintf( '<a href="%s">%s</a>', esc_url( $url ), esc_html( esc_url( $url ) ) );
+}
+
+jetpack_load_shortcodes();
diff --git a/plugins/jetpack/modules/shortcodes/archiveorg-book.php b/plugins/jetpack/modules/shortcodes/archiveorg-book.php
new file mode 100644
index 00000000..88ac328c
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/archiveorg-book.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Archive.org Shortcode
+ *
+ * Usage:
+ * [archiveorg-book goodytwoshoes00newyiala]
+ * [archiveorg-book http://www.archive.org/stream/goodytwoshoes00newyiala]
+ * [archiveorg id=goodytwoshoes00newyiala width=480 height=430]
+
+ * <iframe src='https://www.archive.org/stream/goodytwoshoes00newyiala?ui=embed#mode/1up' width='480px' height='430px' frameborder='0' ></iframe>
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Get ID of requested archive.org book embed.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode attributes.
+ *
+ * @return int|string
+ */
+function jetpack_shortcode_get_archiveorg_book_id( $atts ) {
+ if ( isset( $atts[0] ) ) {
+ $atts[0] = trim( $atts[0], '=' );
+ if ( preg_match( '#archive.org/stream/(.+)/?$#i', $atts[0], $match ) ) {
+ $id = $match[1];
+ } else {
+ $id = $atts[0];
+ }
+ return $id;
+ }
+ return 0;
+}
+
+/**
+ * Convert an archive.org book shortcode into an embed code.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts An array of shortcode attributes.
+ * @return string The embed code for the Archive.org book
+ */
+function jetpack_archiveorg_book_shortcode( $atts ) {
+ global $content_width;
+
+ if ( isset( $atts[0] ) && empty( $atts['id'] ) ) {
+ $atts['id'] = jetpack_shortcode_get_archiveorg_book_id( $atts );
+ }
+
+ $atts = shortcode_atts(
+ array(
+ 'id' => '',
+ 'width' => 480,
+ 'height' => 430,
+ ),
+ $atts
+ );
+
+ if ( ! $atts['id'] ) {
+ return '<!-- error: missing archive.org book ID -->';
+ }
+
+ $id = $atts['id'];
+
+ if ( ! $atts['width'] ) {
+ $width = absint( $content_width );
+ } else {
+ $width = intval( $atts['width'] );
+ }
+
+ if ( ! $atts['height'] ) {
+ $height = round( ( $width / 640 ) * 360 );
+ } else {
+ $height = intval( $atts['height'] );
+ }
+
+ $url = esc_url( set_url_scheme( "http://archive.org/stream/{$id}?ui=embed#mode/1up" ) );
+
+ $html = "<div class='embed-archiveorg-book' style='text-align:center;'><iframe src='$url' width='$width' height='$height' style='border:0;' webkitallowfullscreen='true' mozallowfullscreen='true' allowfullscreen></iframe></div>";
+ return $html;
+}
+
+add_shortcode( 'archiveorg-book', 'jetpack_archiveorg_book_shortcode' );
+
+/**
+ * Compose shortcode from archive.org book iframe.
+ *
+ * @since 4.5.0
+ *
+ * @param string $content Post content.
+ *
+ * @return mixed
+ */
+function jetpack_archiveorg_book_embed_to_shortcode( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, 'archive.org/stream/' ) ) {
+ return $content;
+ }
+
+ $regexp = '!<iframe\s+src=[\'"](http|https)://(www.archive|archive)\.org/stream/([^\'"]+)[\'"]((?:\s+\w+(=[\'"][^\'"]*[\'"])?)*)\s></iframe>!i';
+
+ if ( ! preg_match_all( $regexp, $content, $matches, PREG_SET_ORDER ) ) {
+ return $content;
+ }
+
+ foreach ( $matches as $match ) {
+ $url = explode( '?', $match[3] );
+ $id = $url[0];
+
+ $params = $match[4];
+
+ $params = wp_kses_hair( $params, array( 'http' ) );
+
+ $width = isset( $params['width'] ) ? absint( $params['width']['value'] ) : 0;
+ $height = isset( $params['height'] ) ? absint( $params['height']['value'] ) : 0;
+
+ $wh = '';
+ if ( $width && $height ) {
+ $wh = ' width=' . $width . ' height=' . $height;
+ }
+
+ $shortcode = '[archiveorg-book ' . $id . $wh . ']';
+ $content = str_replace( $match[0], $shortcode, $content );
+ }
+
+ return $content;
+}
+
+add_filter( 'pre_kses', 'jetpack_archiveorg_book_embed_to_shortcode' );
diff --git a/plugins/jetpack/modules/shortcodes/archiveorg.php b/plugins/jetpack/modules/shortcodes/archiveorg.php
new file mode 100644
index 00000000..4a335b32
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/archiveorg.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ * Archive.org book shortcode.
+ *
+ * Usage:
+ * [archiveorg Experime1940]
+ * [archiveorg http://archive.org/details/Experime1940 poster=http://archive.org/images/map.png]
+ * [archiveorg id=Experime1940 width=640 height=480 autoplay=1]
+
+ * <iframe src="http://archive.org/embed/Experime1940&autoplay=1&poster=http://archive.org/images/map.png" width="640" height="480" frameborder="0" webkitallowfullscreen="true" mozallowfullscreen="true" allowfullscreen></iframe>
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Get ID of requested archive.org embed.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode attributes.
+ *
+ * @return int|string
+ */
+function jetpack_shortcode_get_archiveorg_id( $atts ) {
+ if ( isset( $atts[0] ) ) {
+ $atts[0] = trim( $atts[0], '=' );
+ if ( preg_match( '#archive.org/(details|embed)/(.+)/?$#i', $atts[0], $match ) ) {
+ $id = $match[2];
+ } else {
+ $id = $atts[0];
+ }
+ return $id;
+ }
+ return 0;
+}
+
+/**
+ * Convert an archive.org shortcode into an embed code.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts An array of shortcode attributes.
+ * @return string The embed code for the archive.org video.
+ */
+function jetpack_archiveorg_shortcode( $atts ) {
+ global $content_width;
+
+ if ( isset( $atts[0] ) && empty( $atts['id'] ) ) {
+ $atts['id'] = jetpack_shortcode_get_archiveorg_id( $atts );
+ }
+
+ $atts = shortcode_atts(
+ array(
+ 'id' => '',
+ 'width' => 640,
+ 'height' => 480,
+ 'autoplay' => 0,
+ 'poster' => '',
+ ),
+ $atts
+ );
+
+ if ( ! $atts['id'] ) {
+ return '<!-- error: missing archive.org ID -->';
+ }
+
+ $id = $atts['id'];
+
+ if ( ! $atts['width'] ) {
+ $width = absint( $content_width );
+ } else {
+ $width = intval( $atts['width'] );
+ }
+
+ if ( ! $atts['height'] ) {
+ $height = round( ( $width / 640 ) * 360 );
+ } else {
+ $height = intval( $atts['height'] );
+ }
+
+ if ( $atts['autoplay'] ) {
+ $autoplay = '&autoplay=1';
+ } else {
+ $autoplay = '';
+ }
+
+ if ( $atts['poster'] ) {
+ $poster = '&poster=' . $atts['poster'];
+ } else {
+ $poster = '';
+ }
+
+ $url = esc_url( set_url_scheme( "https://archive.org/embed/{$id}{$autoplay}{$poster}" ) );
+
+ $html = "<div class='embed-archiveorg' style='text-align:center;'><iframe src='$url' width='$width' height='$height' style='border:0;' webkitallowfullscreen='true' mozallowfullscreen='true' allowfullscreen></iframe></div>";
+
+ return $html;
+}
+
+add_shortcode( 'archiveorg', 'jetpack_archiveorg_shortcode' );
+
+/**
+ * Compose shortcode from archive.org iframe.
+ *
+ * @since 4.5.0
+ *
+ * @param string $content Post content.
+ *
+ * @return mixed
+ */
+function jetpack_archiveorg_embed_to_shortcode( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, 'archive.org/embed/' ) ) {
+ return $content;
+ }
+
+ $regexp = '!<iframe\s+src=[\'"]https?://archive\.org/embed/([^\'"]+)[\'"]((?:\s+\w+(=[\'"][^\'"]*[\'"])?)*)></iframe>!i';
+
+ if ( ! preg_match_all( $regexp, $content, $matches, PREG_SET_ORDER ) ) {
+ return $content;
+ }
+
+ foreach ( $matches as $match ) {
+ $url = explode( '&amp;', $match[1] );
+ $id = 'id=' . $url[0];
+
+ $autoplay = '';
+ $poster = '';
+ $url_count = count( $url );
+
+ for ( $ii = 1; $ii < $url_count; $ii++ ) {
+ if ( 'autoplay=1' === $url[ $ii ] ) {
+ $autoplay = ' autoplay="1"';
+ }
+
+ $map_matches = array();
+ if ( preg_match( '/^poster=(.+)$/', $url[ $ii ], $map_matches ) ) {
+ $poster = " poster=\"{$map_matches[1]}\"";
+ }
+ }
+
+ $params = $match[2];
+
+ $params = wp_kses_hair( $params, array( 'http' ) );
+
+ $width = isset( $params['width'] ) ? (int) $params['width']['value'] : 0;
+ $height = isset( $params['height'] ) ? (int) $params['height']['value'] : 0;
+
+ $wh = '';
+ if ( $width && $height ) {
+ $wh = ' width=' . $width . ' height=' . $height;
+ }
+
+ $shortcode = '[archiveorg ' . $id . $wh . $autoplay . $poster . ']';
+ $content = str_replace( $match[0], $shortcode, $content );
+ }
+
+ return $content;
+}
+
+add_filter( 'pre_kses', 'jetpack_archiveorg_embed_to_shortcode' );
diff --git a/plugins/jetpack/modules/shortcodes/archives.php b/plugins/jetpack/modules/shortcodes/archives.php
new file mode 100644
index 00000000..881a70f5
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/archives.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Archives shortcode
+ *
+ * @author bubel & nickmomrik
+ * [archives limit=10]
+ *
+ * @package Jetpack
+ */
+
+add_shortcode( 'archives', 'archives_shortcode' );
+
+/**
+ * Display Archives shortcode.
+ *
+ * @param array $atts Shortcode attributes.
+ */
+function archives_shortcode( $atts ) {
+ if ( is_feed() ) {
+ return '[archives]';
+ }
+
+ global $allowedposttags;
+
+ $default_atts = array(
+ 'type' => 'postbypost',
+ 'limit' => '',
+ 'format' => 'html',
+ 'showcount' => false,
+ 'before' => '',
+ 'after' => '',
+ 'order' => 'desc',
+ );
+
+ $attr = shortcode_atts( $default_atts, $atts, 'archives' );
+
+ if ( ! in_array( $attr['type'], array( 'yearly', 'monthly', 'daily', 'weekly', 'postbypost' ), true ) ) {
+ $attr['type'] = 'postbypost';
+ }
+
+ if ( ! in_array( $attr['format'], array( 'html', 'option', 'custom' ), true ) ) {
+ $attr['format'] = 'html';
+ }
+
+ $limit = intval( $attr['limit'] );
+ // A Limit of 0 makes no sense so revert back to the default.
+ if ( empty( $limit ) ) {
+ $limit = '';
+ }
+
+ $showcount = ( false !== $attr['showcount'] && 'false' !== $attr['showcount'] ) ? true : false;
+ $before = wp_kses( $attr['before'], $allowedposttags );
+ $after = wp_kses( $attr['after'], $allowedposttags );
+
+ // Get the archives.
+ $archives = wp_get_archives(
+ array(
+ 'type' => $attr['type'],
+ 'limit' => $limit,
+ 'format' => $attr['format'],
+ 'echo' => false,
+ 'show_post_count' => $showcount,
+ 'before' => $before,
+ 'after' => $after,
+ )
+ );
+
+ if ( 'asc' === $attr['order'] ) {
+ $archives = implode( "\n", array_reverse( explode( "\n", $archives ) ) );
+ }
+
+ // Check to see if there are any archives.
+ if ( empty( $archives ) ) {
+ $archives = '<p>' . __( 'Your blog does not currently have any published posts.', 'jetpack' ) . '</p>';
+ } elseif ( 'option' === $attr['format'] ) {
+ $archives = '<select name="archive-dropdown" onchange="document.location.href=this.options[this.selectedIndex].value;"><option value="' . get_permalink() . '">--</option>' . $archives . '</select>';
+ } elseif ( 'html' === $attr['format'] ) {
+ $archives = '<ul>' . $archives . '</ul>';
+ }
+
+ return $archives;
+}
diff --git a/plugins/jetpack/modules/shortcodes/bandcamp.php b/plugins/jetpack/modules/shortcodes/bandcamp.php
new file mode 100644
index 00000000..36eb9e34
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/bandcamp.php
@@ -0,0 +1,243 @@
+<?php
+/**
+ * Shortcode handler for [bandcamp], which inserts a bandcamp.com
+ * music player (iframe, html5)
+ *
+ * [bandcamp album=119385304]
+ * [bandcamp album=3462839126 bgcol=FFFFFF linkcol=4285BB size=venti]
+ * [bandcamp track=2446959313]
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Display the Bandcamp shortcode.
+ *
+ * @param array $atts Shortcode attributes.
+ */
+function shortcode_handler_bandcamp( $atts ) {
+ // there are no default values, but specify here anyway to explicitly list supported atts.
+ $attributes = shortcode_atts(
+ array(
+ 'album' => null, // integer album id.
+ 'track' => null, // integer track id.
+ 'video' => null, // integer track id for video player.
+ 'size' => 'venti', // one of the supported sizes.
+ 'bgcol' => 'FFFFFF', // hex, no '#' prefix.
+ 'linkcol' => null, // hex, no '#' prefix.
+ 'layout' => null, // encoded layout url.
+ 'width' => null, // integer with optional "%".
+ 'height' => null, // integer with optional "%".
+ 'notracklist' => null, // may be string "true" (defaults false).
+ 'tracklist' => null, // may be string "false" (defaults true).
+ 'artwork' => null, // may be string "false" (alternately: "none") or "small" (default is large).
+ 'minimal' => null, // may be string "true" (defaults false).
+ 'theme' => null, // may be theme identifier string ("light"|"dark" so far).
+ 'package' => null, // integer package id.
+ 't' => null, // integer track number.
+ 'tracks' => null, // comma separated list of allowed tracks.
+ 'esig' => null, // hex, no '#' prefix.
+ ),
+ $atts,
+ 'bandcamp'
+ );
+
+ $sizes = array(
+ 'venti' => array(
+ 'width' => 400,
+ 'height' => 100,
+ ),
+ 'grande' => array(
+ 'width' => 300,
+ 'height' => 100,
+ ),
+ 'grande2' => array(
+ 'width' => 300,
+ 'height' => 355,
+ ),
+ 'grande3' => array(
+ 'width' => 300,
+ 'height' => 415,
+ ),
+ 'tall_album' => array(
+ 'width' => 150,
+ 'height' => 295,
+ ),
+ 'tall_track' => array(
+ 'width' => 150,
+ 'height' => 270,
+ ),
+ 'tall2' => array(
+ 'width' => 150,
+ 'height' => 450,
+ ),
+ 'short' => array(
+ 'width' => 46,
+ 'height' => 23,
+ ),
+ 'large' => array(
+ 'width' => 350,
+ 'height' => 470,
+ ),
+ 'medium' => array(
+ 'width' => 450,
+ 'height' => 120,
+ ),
+ 'small' => array(
+ 'width' => 350,
+ 'height' => 42,
+ ),
+ );
+
+ $sizekey = $attributes['size'];
+ $height = null;
+ $width = null;
+
+ $is_video = false;
+
+ /*
+ * Build iframe url. For audio players, args are appended as
+ * extra path segments for historical reasons having to
+ * do with an IE-only flash bug which required this URL
+ * to contain no querystring. Delay the actual joining
+ * of args into a string until after we decide if it's
+ * a video player or an audio player
+ */
+ $argparts = array();
+
+ if ( ! isset( $attributes['album'] ) && ! isset( $attributes['track'] ) && ! isset( $attributes['video'] ) ) {
+ return "[bandcamp: shortcode must include 'track', 'album', or 'video' param]";
+ }
+
+ if ( isset( $attributes['track'] ) && is_numeric( $attributes['track'] ) ) {
+ $track = esc_attr( $attributes['track'] );
+ array_push( $argparts, "track={$track}" );
+ } elseif ( isset( $attributes['video'] ) && is_numeric( $attributes['video'] ) ) {
+ $track = esc_attr( $attributes['video'] ); // videos are referenced by track id.
+ $url = '//bandcamp.com/EmbeddedPlayer/v=2';
+ $is_video = true;
+ array_push( $argparts, "track={$track}" );
+ }
+ if ( isset( $attributes['album'] ) && is_numeric( $attributes['album'] ) ) {
+ $album = esc_attr( $attributes['album'] );
+ array_push( $argparts, "album={$album}" );
+ }
+
+ if ( 'tall' === $sizekey ) {
+ if ( isset( $attributes['album'] ) ) {
+ $sizekey .= '_album';
+ } else {
+ $sizekey .= '_track';
+ }
+ }
+
+ // if size specified that we don't recognize, fall back on venti.
+ if ( empty( $sizes[ $sizekey ] ) ) {
+ $sizekey = 'venti';
+ $attributes['size'] = 'venti';
+ }
+
+ /*
+ * use strict regex for digits + optional % instead of absint for height/width
+ * 'width' and 'height' params in the iframe url get the exact string from the shortcode
+ * args, whereas the inline style attribute must have "px" added to it if it has no "%"
+ */
+ if ( isset( $attributes['width'] ) && preg_match( '|^([0-9]+)(%)?$|', $attributes['width'], $matches ) ) {
+ $width = $attributes['width'];
+ $csswidth = $attributes['width'];
+ if ( count( $matches ) < 3 ) {
+ $csswidth .= 'px';
+ }
+ }
+ if ( isset( $attributes['height'] ) && preg_match( '|^([0-9]+)(%)?$|', $attributes['height'], $matches ) ) {
+ $height = $attributes['height'];
+ $cssheight = $attributes['height'];
+ if ( count( $matches ) < 3 ) {
+ $cssheight .= 'px';
+ }
+ }
+
+ if ( ! $height ) {
+ $height = $sizes[ $sizekey ]['height'];
+ $cssheight = $height . 'px';
+ }
+
+ if ( ! $width ) {
+ $width = $sizes[ $sizekey ]['width'];
+ $csswidth = $width . 'px';
+ }
+
+ if ( isset( $attributes['layout'] ) ) {
+ array_push( $argparts, "layout={$attributes['layout']}" );
+ } elseif ( isset( $attributes['size'] ) && preg_match( '|^[a-zA-Z0-9]+$|', $attributes['size'] ) ) {
+ array_push( $argparts, "size={$attributes['size']}" );
+ }
+
+ if ( isset( $attributes['bgcol'] ) && preg_match( '|^[0-9A-Fa-f]+$|', $attributes['bgcol'] ) ) {
+ array_push( $argparts, "bgcol={$attributes['bgcol']}" );
+ }
+
+ if ( isset( $attributes['linkcol'] ) && preg_match( '|^[0-9A-Fa-f]+$|', $attributes['linkcol'] ) ) {
+ array_push( $argparts, "linkcol={$attributes['linkcol']}" );
+ }
+
+ if ( isset( $attributes['package'] ) && preg_match( '|^[0-9]+$|', $attributes['package'] ) ) {
+ array_push( $argparts, "package={$attributes['package']}" );
+ }
+
+ if ( isset( $attributes['t'] ) && preg_match( '|^[0-9]+$|', $attributes['t'] ) ) {
+ array_push( $argparts, "t={$attributes['t']}" );
+ }
+
+ if ( 'true' === $attributes['notracklist'] ) {
+ array_push( $argparts, 'notracklist=true' );
+ }
+
+ // 'tracklist' arg deprecates 'notracklist=true' to be less weird. note, behavior
+ // if both are specified is undefined
+ switch ( $attributes['tracklist'] ) {
+ case 'false':
+ case 'none':
+ array_push( $argparts, 'tracklist=false' );
+ break;
+ }
+
+ switch ( $attributes['artwork'] ) {
+ case 'false':
+ case 'none':
+ case 'small':
+ array_push( $argparts, 'artwork=' . $attributes['artwork'] );
+ break;
+ }
+
+ if ( 'true' === $attributes['minimal'] ) {
+ array_push( $argparts, 'minimal=true' );
+ }
+
+ if ( isset( $attributes['theme'] ) && preg_match( '|^[a-zA-Z_]+$|', $attributes['theme'] ) ) {
+ array_push( $argparts, "theme={$attributes['theme']}" );
+ }
+
+ // param 'tracks' is signed digest param 'esig'.
+ if ( isset( $attributes['tracks'] ) && preg_match( '|^[0-9\,]+$|', $attributes['tracks'] ) ) {
+ if ( isset( $attributes['esig'] ) && preg_match( '|^[0-9A-Fa-f]+$|', $attributes['esig'] ) ) {
+ array_push( $argparts, "tracks={$attributes['tracks']}" );
+ array_push( $argparts, "esig={$attributes['esig']}" );
+ }
+ }
+
+ if ( $is_video ) {
+ $url = '//bandcamp.com/VideoEmbed?' . join( '&', $argparts );
+ $extra_attrs = " mozallowfullscreen='1' webkitallowfullscreen='1' allowfullscreen='1'";
+ } else {
+ $url = '//bandcamp.com/EmbeddedPlayer/v=2/' . join( '/', $argparts ) . '/';
+ $extra_attrs = '';
+ }
+
+ $iframe = '<iframe width="%s" height="%s" style="position: relative; display: block; width: %s; height: %s;" src="%s" allowtransparency="true" frameborder="0"%s></iframe>';
+ $iframe = sprintf( $iframe, esc_attr( $width ), esc_attr( $height ), esc_attr( $csswidth ), esc_attr( $cssheight ), esc_url( $url ), $extra_attrs );
+
+ return $iframe;
+}
+
+add_shortcode( 'bandcamp', 'shortcode_handler_bandcamp' );
diff --git a/plugins/jetpack/modules/shortcodes/brightcove.php b/plugins/jetpack/modules/shortcodes/brightcove.php
new file mode 100644
index 00000000..5eca5293
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/brightcove.php
@@ -0,0 +1,295 @@
+<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+
+/**
+ * Brightcove shortcode.
+ *
+ * Brighcove had renovated their video player embedding code since they introduced their "new studio".
+ * See https://support.brightcove.com/en/video-cloud/docs.
+ * The new code is not 100% backward compatible, as long as a customized player is used.
+ * By the time I wrote this, there were about 150000+ posts embedded legacy players, so it would be a bad
+ * idea either to introduce a new brightcove shortcode, or to break those posts completely.
+ *
+ * That's why we introduce a less aggressive way: leaving the old embedding code untouched, and
+ * introduce a new set of shortcode parameters which are translated to the latest Brightcove embedding code.
+ *
+ * e.g.
+ * [brightcove video_id="12345" account_id="99999"] will be translated to the latest embedding code.
+ * [brightcove exp=627045696&vid=1415670151] or [brightcove exp=1463233149&vref=1601200825] will be translated
+ * to the legacy code.
+ */
+class Jetpack_Brightcove_Shortcode {
+ /**
+ * Shortcode name.
+ *
+ * @var string
+ */
+ public static $shortcode = 'brightcove';
+
+ /**
+ * Parse shortcode arguments and render its output.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode parameters.
+ *
+ * @return string
+ */
+ public static function convert( $atts ) {
+ $normalized_atts = self::normalize_attributes( $atts );
+
+ if ( empty( $atts ) ) {
+ return '<!-- Missing Brightcove parameters -->';
+ }
+
+ return self::has_legacy_atts( $normalized_atts )
+ ? self::convert_to_legacy_studio( $normalized_atts )
+ : self::convert_to_new_studio( $normalized_atts );
+ }
+
+ /**
+ * We need to take care of two kinds of shortcode format here.
+ * The latest: [shortcode a=1 b=2] and the legacy: [shortcode a=1&b=2]
+ * For an old shortcode: [shortcode a=1&b=2&c=3], it would be parsed into array( 'a' => 1&b=2&c=3' ), which is useless.
+ * However, since we want to determine whether to call convert_to_legacy_studio() or convert_to_new_studio() via passed parameters, we still need to parse the two properly.
+ * See http://jetpack.wp-a2z.org/oik_api/shortcode_new_to_old_params/
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode parameters.
+ *
+ * @return array
+ */
+ public static function normalize_attributes( $atts ) {
+ if ( is_array( $atts ) && 1 === count( $atts ) ) { // this is the case we need to take care of.
+ $parsed_atts = array();
+ $params = shortcode_new_to_old_params( $atts );
+
+ /**
+ * Filter the Brightcove shortcode parameters.
+ *
+ * @module shortcodes
+ *
+ * @since 4.5.0
+ *
+ * @param string $params String of shortcode parameters.
+ */
+ $params = apply_filters( 'brightcove_dimensions', $params );
+ parse_str( $params, $parsed_atts );
+
+ return $parsed_atts;
+ } else {
+ return $atts;
+ }
+ }
+
+ /**
+ * Check that it has legacy attributes.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode parameters.
+ *
+ * @return bool
+ */
+ public static function has_legacy_atts( $atts ) {
+ return ( isset( $atts['vid'] ) || isset( $atts['vref'] ) )
+ && ( isset( $atts['exp'] ) || isset( $atts['exp3'] ) );
+ }
+
+ /**
+ * Convert to latest player format.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode parameters.
+ *
+ * @return string
+ */
+ public static function convert_to_new_studio( $atts ) {
+ $defaults = array(
+ 'account_id' => '',
+ 'video_id' => '',
+ 'player_id' => 'default',
+ 'width' => '100%',
+ 'height' => '100%',
+ );
+
+ $atts_applied = shortcode_atts( $defaults, $atts, self::$shortcode );
+
+ $player_url = sprintf(
+ '//players.brightcove.net/%s/%s_default/index.html?videoId=%s',
+ esc_attr( $atts_applied['account_id'] ),
+ esc_attr( $atts_applied['player_id'] ),
+ esc_attr( $atts_applied['video_id'] )
+ );
+
+ $output_html = sprintf(
+ '<iframe src="' . esc_url( $player_url ) . '" allowfullscreen webkitallowfullscreen mozallowfullscreen style="width: %spx; height: %spx;"></iframe>',
+ esc_attr( $atts_applied['width'] ),
+ esc_attr( $atts_applied['height'] )
+ );
+
+ return $output_html;
+ }
+
+ /**
+ * Convert to legacy player format.
+ *
+ * [brightcove exp=627045696&vid=1415670151] for the older player and backward compatibility
+ * [brightcove exp=1463233149&vref=1601200825] for the new player
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode parameters.
+ *
+ * @return string
+ */
+ public static function convert_to_legacy_studio( $atts ) {
+ $attr = shortcode_atts(
+ array(
+ 'bg' => '',
+ 'exp' => '',
+ 'exp3' => '',
+ 'h' => '',
+ 'lbu' => '',
+ 'pk' => '',
+ 'pubid' => '',
+ 's' => '',
+ 'surl' => '',
+ 'vid' => '',
+ 'vref' => '',
+ 'w' => '',
+ ),
+ $atts
+ );
+
+ if ( isset( $attr['pk'] ) ) {
+ $attr['pk'] = rawurlencode( preg_replace( '/[^a-zA-Z0-9!*\'();:@&=+$,\/?#\[\]\-_.~ ]/', '', $attr['pk'] ) );
+ }
+
+ if ( isset( $attr['bg'] ) ) {
+ $attr['bg'] = preg_replace( '![^-a-zA-Z0-9#]!', '', $attr['bg'] );
+ }
+
+ $fv = array(
+ 'viewerSecureGatewayURL' => 'https://services.brightcove.com/services/amfgateway',
+ 'servicesURL' => 'http://services.brightcove.com/services',
+ 'cdnURL' => 'http://admin.brightcove.com',
+ 'autoStart' => 'false',
+ );
+
+ $js_tld = 'com';
+ $src = '';
+ $name = 'flashObj';
+ $html5 = false;
+
+ if ( isset( $attr['exp3'] ) ) {
+ if ( isset( $attr['surl'] ) && strpos( $attr['surl'], 'brightcove.co.jp' ) ) {
+ $js_tld = 'co.jp';
+ }
+ if ( ! isset( $attr['surl'] ) || ! preg_match( '#^https?://(?:[a-z\d-]+\.)*brightcove\.(?:com|co\.jp)/#', $attr['surl'] ) ) {
+ $attr['surl'] = 'http://c.brightcove.com/services';
+ }
+
+ $attr['exp3'] = intval( $attr['exp3'] );
+ $attr['pubid'] = intval( $attr['pubid'] );
+ $attr['vid'] = intval( $attr['vid'] );
+
+ $fv['servicesURL'] = $attr['surl'];
+ $fv['playerID'] = $attr['exp3'];
+ $fv['domain'] = 'embed';
+ $fv['videoID'] = intval( $attr['vid'] );
+
+ $src = sprintf(
+ '%s/viewer/federated_f9/%s?isVid=1&amp;isUI=1&amp;publisherID=%s',
+ $attr['surl'],
+ $attr['exp3'],
+ $attr['pubid']
+ );
+ $html5 = true;
+ } elseif ( isset( $attr['exp'] ) ) {
+ $attr['exp'] = intval( $attr['exp'] );
+ $src = 'http://services.brightcove.com/services/viewer/federated_f8/' . $attr['exp'];
+ if ( $attr['vid'] ) {
+ $fv['videoId'] = $attr['vid'];
+ } elseif ( $attr['vref'] ) {
+ $fv['videoRef'] = $attr['vref'];
+ }
+
+ $fv['playerId'] = $attr['exp'];
+ $fv['domain'] = 'embed';
+ } else {
+ return '<small>brightcove error: missing required parameter exp or exp3</small>';
+ }
+
+ if ( ! empty( $attr['lbu'] ) ) {
+ $fv['linkBaseURL'] = $attr['lbu'];
+ }
+
+ $flashvars = trim( add_query_arg( array_map( 'urlencode', $fv ), '' ), '?' );
+
+ $width = null;
+ $height = null;
+
+ if ( ! empty( $attr['w'] ) && ! empty( $attr['h'] ) ) {
+ $w = abs( (int) $attr['w'] );
+ $h = abs( (int) $attr['h'] );
+ if ( $w && $h ) {
+ $width = $w;
+ $height = $h;
+ }
+ } elseif ( empty( $attr['s'] ) || 'l' === $attr['s'] ) {
+ $width = '480';
+ $height = '360';
+ }
+
+ if ( empty( $width ) || empty( $height ) ) {
+ $width = '280';
+ $height = '210';
+ }
+
+ if ( $html5 ) {
+ wp_enqueue_script(
+ 'brightcove-loader',
+ Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/brightcove.min.js', 'modules/shortcodes/js/brightcove.js' ),
+ array( 'jquery' ),
+ 20121127,
+ false
+ );
+ wp_localize_script(
+ 'brightcove-loader',
+ 'brightcoveData',
+ array(
+ 'tld' => esc_js( $js_tld ),
+ )
+ );
+
+ return '
+ <object id="myExperience" class="BrightcoveExperience">
+ <param name="bgcolor" value="' . esc_attr( $attr['bg'] ) . '" />
+ <param name="width" value="' . esc_attr( $width ) . '" />
+ <param name="height" value="' . esc_attr( $height ) . '" />
+ <param name="playerID" value="' . esc_attr( $attr['exp3'] ) . '" />
+ <param name="@videoPlayer" value="' . esc_attr( $attr['vid'] ) . '" />
+ <param name="playerKey" value="' . esc_attr( $attr['pk'] ) . '" />
+ <param name="isVid" value="1" />
+ <param name="isUI" value="1" />
+ <param name="dynamicStreaming" value="true" />
+ <param name="autoStart" value="false" />
+ <param name="secureConnections" value="true" />
+ <param name="secureHTMLConnections" value="true" />
+ </object>';
+ }
+
+ return sprintf(
+ '<embed src="%s" bgcolor="#FFFFFF" flashvars="%s" base="http://admin.brightcove.com" name="%s" width="%s" height="%s" allowFullScreen="true" seamlesstabbing="false" type="application/x-shockwave-flash" swLiveConnect="true" pluginspage="http://www.macromedia.com/shockwave/download/index.cgi?P1_Prod_Version=ShockwaveFlash" />',
+ esc_url( $src ),
+ $flashvars,
+ esc_attr( $name ),
+ esc_attr( $width ),
+ esc_attr( $height )
+ );
+ }
+}
+
+add_shortcode( Jetpack_Brightcove_Shortcode::$shortcode, array( 'Jetpack_Brightcove_Shortcode', 'convert' ) );
diff --git a/plugins/jetpack/modules/shortcodes/cartodb.php b/plugins/jetpack/modules/shortcodes/cartodb.php
new file mode 100644
index 00000000..4cd1266b
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/cartodb.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Carto (formerly CartoDB)
+ *
+ * Example URL: http://osm2.carto.com/viz/08aef918-94da-11e4-ad83-0e0c41326911/public_map
+ *
+ * possible patterns:
+ * [username].carto.com/viz/[map-id]/public_map
+ * [username].carto.com/viz/[map-id]/embed_map
+ * [username].carto.com/viz/[map-id]/map
+ * [organization].carto.com/u/[username]/viz/[map-id]/public_map
+ * [organization].carto.com/u/[username]/viz/[map-id]/embed_map
+ * [organization].carto.com/u/[username]/viz/[map-id]/map
+ *
+ * On July 8th, 2016 CartoDB changed its primary domain from cartodb.com to carto.com
+ * So this shortcode still supports the cartodb.com domain for oembeds.
+ *
+ * @package Jetpack
+ */
+
+wp_oembed_add_provider( '#https?://(?:www\.)?[^/^\.]+\.carto(db)?\.com/\S+#i', 'https://services.carto.com/oembed', true );
diff --git a/plugins/jetpack/modules/shortcodes/class.filter-embedded-html-objects.php b/plugins/jetpack/modules/shortcodes/class.filter-embedded-html-objects.php
new file mode 100644
index 00000000..43e6ca18
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/class.filter-embedded-html-objects.php
@@ -0,0 +1,400 @@
+<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * The companion file to shortcodes.php
+ *
+ * This file contains the code that converts HTML embeds into shortcodes
+ * for when the user copy/pastes in HTML.
+ *
+ * @package Jetpack
+ */
+
+add_filter( 'pre_kses', array( 'Filter_Embedded_HTML_Objects', 'filter' ), 11 );
+add_filter( 'pre_kses', array( 'Filter_Embedded_HTML_Objects', 'maybe_create_links' ), 100 ); // See WPCom_Embed_Stats::init().
+
+/**
+ * Helper class for identifying and parsing known HTML embeds (iframe, object, embed, etc. elements), then converting them to shortcodes.
+ * For unknown HTML embeds, the class still tries to convert them to plain links so that at least something is preserved instead of having the entire element stripped by KSES.
+ *
+ * @since 4.5.0
+ */
+class Filter_Embedded_HTML_Objects {
+ /**
+ * Array of patterns to search for via strpos().
+ * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
+ * Patterns are matched against URLs (src or movie HTML attributes).
+ *
+ * @var array
+ */
+ public static $strpos_filters = array();
+ /**
+ * Array of patterns to search for via preg_match().
+ * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
+ * Patterns are matched against URLs (src or movie HTML attributes).
+ *
+ * @var array
+ */
+ public static $regexp_filters = array();
+ /**
+ * HTML element being processed.
+ *
+ * @var string
+ */
+ public static $current_element = false;
+ /**
+ * Array of patterns to search for via strpos().
+ * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
+ * Patterns are matched against full HTML elements.
+ *
+ * @var array
+ */
+ public static $html_strpos_filters = array();
+ /**
+ * Array of patterns to search for via preg_match().
+ * Keys are patterns, values are callback functions that implement the HTML -> shortcode replacement.
+ * Patterns are matched against full HTML elements.
+ *
+ * @var array
+ */
+ public static $html_regexp_filters = array();
+ /**
+ * Failed embeds (stripped)
+ *
+ * @var array
+ */
+ public static $failed_embeds = array();
+
+ /**
+ * Store tokens found in Syntax Highlighter.
+ *
+ * @since 4.5.0
+ *
+ * @var array
+ */
+ private static $sh_unfiltered_content_tokens;
+
+ /**
+ * Capture tokens found in Syntax Highlighter and collect them in self::$sh_unfiltered_content_tokens.
+ *
+ * @since 4.5.0
+ *
+ * @param array $match Array of Syntax Highlighter matches.
+ *
+ * @return string
+ */
+ public static function sh_regexp_callback( $match ) {
+ $token = sprintf(
+ '[prekses-filter-token-%1$d-%2$s-%1$d]',
+ wp_rand(),
+ md5( $match[0] )
+ );
+ self::$sh_unfiltered_content_tokens[ $token ] = $match[0];
+ return $token;
+ }
+
+ /**
+ * Look for HTML elements that match the registered patterns.
+ * Replace them with the HTML generated by the registered replacement callbacks.
+ *
+ * @param string $html Post content.
+ */
+ public static function filter( $html ) {
+ if ( ! $html || ! is_string( $html ) ) {
+ return $html;
+ }
+
+ $regexps = array(
+ 'object' => '%<object[^>]*+>(?>[^<]*+(?><(?!/object>)[^<]*+)*)</object>%i',
+ 'embed' => '%<embed[^>]*+>(?:\s*</embed>)?%i',
+ 'iframe' => '%<iframe[^>]*+>(?>[^<]*+(?><(?!/iframe>)[^<]*+)*)</iframe>%i',
+ 'div' => '%<div[^>]*+>(?>[^<]*+(?><(?!/div>)[^<]*+)*+)(?:</div>)+%i',
+ 'script' => '%<script[^>]*+>(?>[^<]*+(?><(?!/script>)[^<]*+)*)</script>%i',
+ );
+
+ $unfiltered_content_tokens = array();
+ self::$sh_unfiltered_content_tokens = array();
+
+ // Check here to make sure that SyntaxHighlighter is still used. (Just a little future proofing).
+ if ( class_exists( 'SyntaxHighlighter' ) ) {
+ /*
+ * Replace any "code" shortcode blocks with a token that we'll later replace with its original text.
+ * This will keep the contents of the shortcode from being filtered.
+ */
+ global $SyntaxHighlighter; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
+
+ // Check to see if the $syntax_highlighter object has been created and is ready for use.
+ if ( isset( $SyntaxHighlighter ) && is_array( $SyntaxHighlighter->shortcodes ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
+ $shortcode_regex = implode( '|', array_map( 'preg_quote', $SyntaxHighlighter->shortcodes ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
+ $html = preg_replace_callback(
+ '/\[(' . $shortcode_regex . ')(\s[^\]]*)?\][\s\S]*?\[\/\1\]/m',
+ array( __CLASS__, 'sh_regexp_callback' ),
+ $html
+ );
+ $unfiltered_content_tokens = self::$sh_unfiltered_content_tokens;
+ }
+ }
+
+ foreach ( $regexps as $element => $regexp ) {
+ self::$current_element = $element;
+
+ if ( false !== stripos( $html, "<$element" ) ) {
+ $new_html = preg_replace_callback( $regexp, array( __CLASS__, 'dispatch' ), $html );
+ if ( $new_html ) {
+ $html = $new_html;
+ }
+ }
+
+ if ( false !== stripos( $html, "&lt;$element" ) ) {
+ $regexp_entities = self::regexp_entities( $regexp );
+ $new_html = preg_replace_callback( $regexp_entities, array( __CLASS__, 'dispatch_entities' ), $html );
+ if ( $new_html ) {
+ $html = $new_html;
+ }
+ }
+ }
+
+ if ( count( $unfiltered_content_tokens ) > 0 ) {
+ // Replace any tokens generated earlier with their original unfiltered text.
+ $html = str_replace( array_keys( $unfiltered_content_tokens ), $unfiltered_content_tokens, $html );
+ }
+
+ return $html;
+ }
+
+ /**
+ * Replace HTML entities in current HTML element regexp.
+ * This is useful when the content is HTML encoded by TinyMCE.
+ *
+ * @param string $regexp Selected regexp.
+ */
+ public static function regexp_entities( $regexp ) {
+ return preg_replace(
+ '/\[\^&([^\]]+)\]\*\+/',
+ '(?>[^&]*+(?>&(?!\1)[^&])*+)*+',
+ str_replace( '?&gt;', '?' . '>', htmlspecialchars( $regexp, ENT_NOQUOTES ) )
+ );
+ }
+
+ /**
+ * Register a filter to convert a matching HTML element to a shortcode.
+ *
+ * We can match the provided pattern against the source URL of the HTML element
+ * (generally the value of the src attribute of the HTML element), or against the full HTML element.
+ *
+ * The callback is passed an array containing the raw HTML of the element as well as pre-parsed attribute name/values.
+ *
+ * @param string $match Pattern to search for: either a regular expression to use with preg_match() or a search string to use with strpos().
+ * @param string $callback Function used to convert embed into shortcode.
+ * @param bool $is_regexp Is $match a regular expression? If true, match using preg_match(). If not, match using strpos(). Default false.
+ * @param bool $is_html_filter Match the pattern against the full HTML (true) or just the source URL (false)? Default false.
+ */
+ public static function register( $match, $callback, $is_regexp = false, $is_html_filter = false ) {
+ if ( $is_html_filter ) {
+ if ( $is_regexp ) {
+ self::$html_regexp_filters[ $match ] = $callback;
+ } else {
+ self::$html_strpos_filters[ $match ] = $callback;
+ }
+ } else {
+ if ( $is_regexp ) {
+ self::$regexp_filters[ $match ] = $callback;
+ } else {
+ self::$strpos_filters[ $match ] = $callback;
+ }
+ }
+ }
+
+ /**
+ * Delete an existing registered pattern/replacement filter.
+ *
+ * @param string $match Embed regexp.
+ */
+ public static function unregister( $match ) {
+ // Allow themes/plugins to remove registered embeds.
+ unset( self::$regexp_filters[ $match ] );
+ unset( self::$strpos_filters[ $match ] );
+ unset( self::$html_regexp_filters[ $match ] );
+ unset( self::$html_strpos_filters[ $match ] );
+ }
+
+ /**
+ * Filter and replace HTML element entity.
+ *
+ * @param array $matches Array of matches.
+ */
+ private static function dispatch_entities( $matches ) {
+ $matches[0] = html_entity_decode( $matches[0] );
+
+ return self::dispatch( $matches );
+ }
+
+ /**
+ * Filter and replace HTML element.
+ *
+ * @param array $matches Array of matches.
+ */
+ private static function dispatch( $matches ) {
+ $html = preg_replace( '%&#0*58;//%', '://', $matches[0] );
+ $attrs = self::get_attrs( $html );
+ if ( isset( $attrs['src'] ) ) {
+ $src = $attrs['src'];
+ } elseif ( isset( $attrs['movie'] ) ) {
+ $src = $attrs['movie'];
+ } else {
+ // no src found, search html.
+ foreach ( self::$html_strpos_filters as $match => $callback ) {
+ if ( false !== strpos( $html, $match ) ) {
+ return call_user_func( $callback, $attrs );
+ }
+ }
+
+ foreach ( self::$html_regexp_filters as $match => $callback ) {
+ if ( preg_match( $match, $html ) ) {
+ return call_user_func( $callback, $attrs );
+ }
+ }
+
+ return $matches[0];
+ }
+
+ $src = trim( $src );
+
+ // check source filter.
+ foreach ( self::$strpos_filters as $match => $callback ) {
+ if ( false !== strpos( $src, $match ) ) {
+ return call_user_func( $callback, $attrs );
+ }
+ }
+
+ foreach ( self::$regexp_filters as $match => $callback ) {
+ if ( preg_match( $match, $src ) ) {
+ return call_user_func( $callback, $attrs );
+ }
+ }
+
+ // check html filters.
+ foreach ( self::$html_strpos_filters as $match => $callback ) {
+ if ( false !== strpos( $html, $match ) ) {
+ return call_user_func( $callback, $attrs );
+ }
+ }
+
+ foreach ( self::$html_regexp_filters as $match => $callback ) {
+ if ( preg_match( $match, $html ) ) {
+ return call_user_func( $callback, $attrs );
+ }
+ }
+
+ // Log the strip.
+ if ( function_exists( 'wp_kses_reject' ) ) {
+ wp_kses_reject(
+ sprintf(
+ /* translators: placeholder is an HTML tag. */
+ __( '<code>%s</code> HTML tag removed as it is not allowed', 'jetpack' ),
+ '&lt;' . self::$current_element . '&gt;'
+ ),
+ array( self::$current_element => $attrs )
+ );
+ }
+
+ // Keep the failed match so we can later replace it with a link,
+ // but return the original content to give others a chance too.
+ self::$failed_embeds[] = array(
+ 'match' => $matches[0],
+ 'src' => esc_url( $src ),
+ );
+
+ return $matches[0];
+ }
+
+ /**
+ * Failed embeds are stripped, so let's convert them to links at least.
+ *
+ * @param string $string Failed embed string.
+ *
+ * @return string $string Linkified string.
+ */
+ public static function maybe_create_links( $string ) {
+ if ( empty( self::$failed_embeds ) ) {
+ return $string;
+ }
+
+ foreach ( self::$failed_embeds as $entry ) {
+ $html = sprintf( '<a href="%s">%s</a>', esc_url( $entry['src'] ), esc_url( $entry['src'] ) );
+ // Check if the string doesn't contain iframe, before replace.
+ if ( ! preg_match( '/<iframe /', $string ) ) {
+ $string = str_replace( $entry['match'], $html, $string );
+ }
+ }
+
+ self::$failed_embeds = array();
+
+ return $string;
+ }
+
+ /**
+ * Parse post HTML for HTML tags.
+ *
+ * @param string $html Post HTML.
+ */
+ public static function get_attrs( $html ) {
+ if (
+ ! ( class_exists( 'DOMDocument' ) && function_exists( 'libxml_use_internal_errors' ) && function_exists( 'simplexml_load_string' ) ) ) {
+ trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
+ esc_html__( 'PHP’s XML extension is not available. Please contact your hosting provider to enable PHP’s XML extension.', 'jetpack' )
+ );
+ return array();
+ }
+ // We have to go through DOM, since it can load non-well-formed XML (i.e. HTML). SimpleXML cannot.
+ $dom = new DOMDocument();
+ // The @ is not enough to suppress errors when dealing with libxml,
+ // we have to tell it directly how we want to handle errors.
+ libxml_use_internal_errors( true );
+ // Suppress parser warnings.
+ @$dom->loadHTML( $html ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+ libxml_use_internal_errors( false );
+ $xml = false;
+ // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
+ foreach ( $dom->childNodes as $node ) {
+ // find the root node (html).
+ if ( XML_ELEMENT_NODE === $node->nodeType ) {
+ /*
+ * Use simplexml_load_string rather than simplexml_import_dom
+ * as the later doesn't cope well if the XML is malformmed in the DOM
+ * See #1688-wpcom.
+ */
+ libxml_use_internal_errors( true );
+ // html->body->object.
+ $xml = simplexml_load_string( $dom->saveXML( $node->firstChild->firstChild ) );
+ libxml_clear_errors();
+ break;
+ }
+ }
+ // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
+
+ if ( ! $xml ) {
+ return array();
+ }
+
+ $attrs = array();
+ $attrs['_raw_html'] = $html;
+
+ // <param> elements
+ foreach ( $xml->param as $param ) {
+ $attrs[ (string) $param['name'] ] = (string) $param['value'];
+ }
+
+ // <object> attributes
+ foreach ( $xml->attributes() as $name => $attr ) {
+ $attrs[ $name ] = (string) $attr;
+ }
+
+ // <embed> attributes
+ if ( $xml->embed ) {
+ foreach ( $xml->embed->attributes() as $name => $attr ) {
+ $attrs[ $name ] = (string) $attr;
+ }
+ }
+
+ return $attrs;
+ }
+}
diff --git a/plugins/jetpack/modules/shortcodes/codepen.php b/plugins/jetpack/modules/shortcodes/codepen.php
new file mode 100644
index 00000000..8988bc18
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/codepen.php
@@ -0,0 +1,11 @@
+<?php
+/**
+ * CodePen embed
+ *
+ * Example URL: http://codepen.io/css-tricks/pen/wFeaG
+ *
+ * @package Jetpack
+ */
+
+// Register oEmbed provider.
+wp_oembed_add_provider( '#https?://codepen.io/([^/]+)/pen/([^/]+)/?#', 'https://codepen.io/api/oembed', true );
diff --git a/plugins/jetpack/modules/shortcodes/crowdsignal.php b/plugins/jetpack/modules/shortcodes/crowdsignal.php
new file mode 100644
index 00000000..e2078a68
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/crowdsignal.php
@@ -0,0 +1,610 @@
+<?php
+
+// Keep compatibility with polldaddy-plugin
+if ( ! class_exists( 'CrowdsignalShortcode' ) && ! class_exists( 'PolldaddyShortcode' ) ) {
+
+/**
+* Class wrapper for Crowdsignal shortcodes
+*/
+
+class CrowdsignalShortcode {
+
+ static $add_script = false;
+ static $scripts = false;
+
+ /**
+ * Add all the actions & resgister the shortcode
+ */
+ function __construct() {
+ if ( defined( 'GLOBAL_TAGS' ) == false ) {
+ add_shortcode( 'crowdsignal', array( $this, 'crowdsignal_shortcode' ) );
+ add_shortcode( 'polldaddy', array( $this, 'crowdsignal_shortcode' ) );
+
+ add_filter( 'pre_kses', array( $this, 'crowdsignal_embed_to_shortcode' ) );
+ }
+ add_action( 'wp_enqueue_scripts', array( $this, 'check_infinite' ) );
+ add_action( 'infinite_scroll_render', array( $this, 'crowdsignal_shortcode_infinite' ), 11 );
+ }
+
+ private function get_async_code( array $settings, $survey_link ) {
+ $include = <<<CONTAINER
+( function( d, c, j ) {
+ if ( !d.getElementById( j ) ) {
+ var pd = d.createElement( c ), s;
+ pd.id = j;
+ pd.src = 'https://polldaddy.com/survey.js';
+ s = d.getElementsByTagName( c )[0];
+ s.parentNode.insertBefore( pd, s );
+ }
+}( document, 'script', 'pd-embed' ) );
+CONTAINER;
+
+ // Compress it a bit
+ $include = $this->compress_it( $include );
+
+ $placeholder =
+ '<div class="cs-embed pd-embed" data-settings="'
+ . esc_attr( json_encode( $settings ) )
+ . '"></div>';
+ if ( 'button' === $settings['type'] ) {
+ $placeholder =
+ '<a class="cs-embed pd-embed" href="'
+ . esc_attr( $survey_link )
+ . '" data-settings="'
+ . esc_attr( json_encode( $settings ) )
+ . '">'
+ . esc_html( $settings['title'] )
+ . '</a>';
+ }
+
+ $js_include = $placeholder . "\n";
+ $js_include .= '<script type="text/javascript"><!--//--><![CDATA[//><!--' . "\n";
+ $js_include .= $include . "\n";
+ $js_include .= "//--><!]]></script>\n";
+
+ if ( 'button' !== $settings['type'] ) {
+ $js_include .= '<noscript>' . $survey_link . "</noscript>\n";
+ }
+
+ return $js_include;
+ }
+
+ private function compress_it( $js ) {
+ $js = str_replace( array( "\n", "\t", "\r" ), '', $js );
+ $js = preg_replace( '/\s*([,:\?\{;\-=\(\)])\s*/', '$1', $js );
+ return $js;
+ }
+
+ /*
+ * Crowdsignal Poll Embed script - transforms code that looks like that:
+ * <script type="text/javascript" charset="utf-8" async src="http://static.polldaddy.com/p/123456.js"></script>
+ * <noscript><a href="http://polldaddy.com/poll/123456/">What is your favourite color?</a></noscript>
+ * into the [crowdsignal poll=...] shortcode format
+ */
+ function crowdsignal_embed_to_shortcode( $content ) {
+
+ if ( ! is_string( $content ) || false === strpos( $content, 'polldaddy.com/p/' ) ) {
+ return $content;
+ }
+
+ $regexes = array();
+
+ $regexes[] = '#<script[^>]+?src="https?://(secure|static)\.polldaddy\.com/p/([0-9]+)\.js"[^>]*+>\s*?</script>\r?\n?(<noscript>.*?</noscript>)?#i';
+
+ $regexes[] = '#&lt;script(?:[^&]|&(?!gt;))+?src="https?://(secure|static)\.polldaddy\.com/p/([0-9]+)\.js"(?:[^&]|&(?!gt;))*+&gt;\s*?&lt;/script&gt;\r?\n?(&lt;noscript&gt;.*?&lt;/noscript&gt;)?#i';
+
+ foreach ( $regexes as $regex ) {
+ if ( ! preg_match_all( $regex, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ if ( ! isset( $match[2] ) ) {
+ continue;
+ }
+
+ $id = (int) $match[2];
+
+ if ( $id > 0 ) {
+ $content = str_replace( $match[0], " [crowdsignal poll=$id]", $content );
+ /** This action is documented in modules/shortcodes/youtube.php */
+ do_action( 'jetpack_embed_to_shortcode', 'crowdsignal', $id );
+ }
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Shortcode for polldadddy
+ * [crowdsignal poll|survey|rating="123456"]
+ */
+ function crowdsignal_shortcode( $atts ) {
+ global $post;
+ global $content_width;
+
+ /**
+ * Variables extracted from $atts.
+ *
+ * @var string $survey
+ * @var string $link_text
+ * @var string $poll
+ * @var string $rating
+ * @var string $unique_id
+ * @var string $item_id
+ * @var string $title
+ * @var string $permalink
+ * @var int $cb
+ * @var string $type
+ * @var string $body
+ * @var string $button
+ * @var string $text_color
+ * @var string $back_color
+ * @var string $align
+ * @var string $style
+ * @var int $width
+ * @var int $height
+ * @var int $delay
+ * @var string $visit
+ * @var string $domain
+ * @var string $id
+ */
+ extract( shortcode_atts( array(
+ 'survey' => null,
+ 'link_text' => 'Take Our Survey',
+ 'poll' => 'empty',
+ 'rating' => 'empty',
+ 'unique_id' => null,
+ 'item_id' => null,
+ 'title' => null,
+ 'permalink' => null,
+ 'cb' => 0,
+ 'type' => 'button',
+ 'body' => '',
+ 'button' => '',
+ 'text_color' => '000000',
+ 'back_color' => 'FFFFFF',
+ 'align' => '',
+ 'style' => '',
+ 'width' => $content_width,
+ 'height' => floor( $content_width * 3 / 4 ),
+ 'delay' => 100,
+ 'visit' => 'single',
+ 'domain' => '',
+ 'id' => '',
+ ), $atts, 'crowdsignal' ) );
+
+ if ( ! is_array( $atts ) ) {
+ return '<!-- Crowdsignal shortcode passed invalid attributes -->';
+ }
+
+ $inline = ! in_the_loop();
+ $no_script = false;
+ $infinite_scroll = false;
+
+ if ( is_home() && current_theme_supports( 'infinite-scroll' ) ) {
+ $infinite_scroll = true;
+ }
+
+ if ( defined( 'PADPRESS_LOADED' ) ) {
+ $inline = true;
+ }
+
+ if ( function_exists( 'get_option' ) && get_option( 'polldaddy_load_poll_inline' ) ) {
+ $inline = true;
+ }
+
+ if ( is_feed() || ( defined( 'DOING_AJAX' ) && ! $infinite_scroll ) ) {
+ $no_script = false;
+ }
+
+ self::$add_script = $infinite_scroll;
+
+ if ( intval( $rating ) > 0 && ! $no_script ) { //rating embed
+
+ if ( empty( $unique_id ) ) {
+ $unique_id = is_page() ? 'wp-page-' . $post->ID : 'wp-post-' . $post->ID;
+ }
+
+ if ( empty( $item_id ) ) {
+ $item_id = is_page() ? '_page_' . $post->ID : '_post_' . $post->ID;
+ }
+
+ if ( empty( $title ) ) {
+ /** This filter is documented in core/src/wp-includes/general-template.php */
+ $title = apply_filters( 'wp_title', $post->post_title, '', '' );
+ }
+
+ if ( empty( $permalink ) ) {
+ $permalink = get_permalink( $post->ID );
+ }
+
+ $rating = intval( $rating );
+ $unique_id = preg_replace( '/[^\-_a-z0-9]/i', '', wp_strip_all_tags( $unique_id ) );
+ $item_id = wp_strip_all_tags( $item_id );
+ $item_id = preg_replace( '/[^_a-z0-9]/i', '', $item_id );
+
+ $settings = json_encode( array(
+ 'id' => $rating,
+ 'unique_id' => $unique_id,
+ 'title' => rawurlencode( trim( $title ) ),
+ 'permalink' => esc_url( $permalink ),
+ 'item_id' => $item_id,
+ ) );
+
+ $item_id = esc_js( $item_id );
+
+ if ( Jetpack_AMP_Support::is_amp_request() ) {
+ return sprintf( '<a href="%s" target="_blank">%s</a>', esc_url( $permalink ), esc_html( trim( $title ) ) );
+ } elseif ( $inline ) {
+ return <<<SCRIPT
+<div class="cs-rating pd-rating" id="pd_rating_holder_{$rating}{$item_id}"></div>
+<script type="text/javascript" charset="UTF-8"><!--//--><![CDATA[//><!--
+PDRTJS_settings_{$rating}{$item_id}={$settings};
+//--><!]]></script>
+<script type="text/javascript" charset="UTF-8" async src="https://polldaddy.com/js/rating/rating.js"></script>
+SCRIPT;
+ } else {
+ if ( false === self::$scripts ) {
+ self::$scripts = array();
+ }
+
+ $data = array( 'id' => $rating, 'item_id' => $item_id, 'settings' => $settings );
+
+ self::$scripts['rating'][] = $data;
+
+ add_action( 'wp_footer', array( $this, 'generate_scripts' ) );
+
+ $data = esc_attr( json_encode( $data ) );
+
+ if ( $infinite_scroll ) {
+ return <<<CONTAINER
+<div class="cs-rating pd-rating" id="pd_rating_holder_{$rating}{$item_id}" data-settings="{$data}"></div>
+CONTAINER;
+ } else {
+ return <<<CONTAINER
+<div class="cs-rating pd-rating" id="pd_rating_holder_{$rating}{$item_id}"></div>
+CONTAINER;
+ }
+ }
+ } elseif ( intval( $poll ) > 0 ) { //poll embed
+
+ if ( empty( $title ) ) {
+ $title = __( 'Take Our Poll', 'jetpack' );
+ }
+
+ $poll = intval( $poll );
+ $poll_url = sprintf( 'https://poll.fm/%d', $poll );
+ $poll_js = sprintf( 'https://secure.polldaddy.com/p/%d.js', $poll );
+ $poll_link = sprintf( '<a href="%s" target="_blank">%s</a>', esc_url( $poll_url ), esc_html( $title ) );
+
+ if ( $no_script || Jetpack_AMP_Support::is_amp_request() ) {
+ return $poll_link;
+ } else {
+ if ( $type == 'slider' && !$inline ) {
+
+ if ( ! in_array( $visit, array( 'single', 'multiple' ) ) ) {
+ $visit = 'single';
+ }
+
+ $settings = array(
+ 'type' => 'slider',
+ 'embed' => 'poll',
+ 'delay' => intval( $delay ),
+ 'visit' => $visit,
+ 'id' => intval( $poll )
+ );
+
+ return $this->get_async_code( $settings, $poll_link );
+ } else {
+ $cb = ( $cb == 1 ? '?cb='.mktime() : false );
+ $margins = '';
+ $float = '';
+
+ if ( in_array( $align, array( 'right', 'left' ) ) ) {
+ $float = sprintf( 'float: %s;', $align );
+
+ if ( $align == 'left')
+ $margins = 'margin: 0px 10px 0px 0px;';
+ elseif ( $align == 'right' )
+ $margins = 'margin: 0px 0px 0px 10px';
+ }
+
+ // Force the normal style embed on single posts/pages otherwise it's not rendered on infinite scroll themed blogs ('infinite_scroll_render' isn't fired)
+ if ( is_singular() ) {
+ $inline = true;
+ }
+
+ if ( false === $cb && ! $inline ) {
+ if ( false === self::$scripts ) {
+ self::$scripts = array();
+ }
+
+ $data = array( 'url' => $poll_js );
+
+ self::$scripts['poll'][intval( $poll )] = $data;
+
+ add_action( 'wp_footer', array( $this, 'generate_scripts' ) );
+
+ $data = esc_attr( json_encode( $data ) );
+
+ $script_url = esc_url_raw( plugins_url( 'js/polldaddy-shortcode.js', __FILE__ ) );
+ $str = <<<CONTAINER
+<a name="pd_a_{$poll}"></a>
+<div class="CSS_Poll PDS_Poll" id="PDI_container{$poll}" data-settings="{$data}" style="display:inline-block;{$float}{$margins}"></div>
+<div id="PD_superContainer"></div>
+<noscript>{$poll_link}</noscript>
+CONTAINER;
+
+$loader = <<<SCRIPT
+( function( d, c, j ) {
+ if ( ! d.getElementById( j ) ) {
+ var pd = d.createElement( c ), s;
+ pd.id = j;
+ pd.src = '{$script_url}';
+ s = d.getElementsByTagName( c )[0];
+ s.parentNode.insertBefore( pd, s );
+ } else if ( typeof jQuery !== 'undefined' ) {
+ jQuery( d.body ).trigger( 'pd-script-load' );
+ }
+} ( document, 'script', 'pd-polldaddy-loader' ) );
+SCRIPT;
+
+ $loader = $this->compress_it( $loader );
+ $loader = "<script type='text/javascript'>\n" . $loader . "\n</script>";
+
+ return $str . $loader;
+ } else {
+ if ( $inline ) {
+ $cb = '';
+ }
+
+ return <<<CONTAINER
+<a id="pd_a_{$poll}"></a>
+<div class="CSS_Poll PDS_Poll" id="PDI_container{$poll}" style="display:inline-block;{$float}{$margins}"></div>
+<div id="PD_superContainer"></div>
+<script type="text/javascript" charset="UTF-8" async src="{$poll_js}{$cb}"></script>
+<noscript>{$poll_link}</noscript>
+CONTAINER;
+ }
+ }
+ }
+ } elseif ( ! empty( $survey ) ) { //survey embed
+
+ if ( in_array( $type, array( 'iframe', 'button', 'banner', 'slider' ) ) ) {
+
+ if ( empty( $title ) ) {
+ $title = __( 'Take Our Survey', 'jetpack' );
+ if( ! empty( $link_text ) ) {
+ $title = $link_text;
+ }
+ }
+
+ if ( $type == 'banner' || $type == 'slider' )
+ $inline = false;
+
+ $survey = preg_replace( '/[^a-f0-9]/i', '', $survey );
+ $survey_url = esc_url( "https://survey.fm/{$survey}" );
+ $survey_link = sprintf( '<a href="%s" target="_blank">%s</a>', $survey_url, esc_html( $title ) );
+
+ $settings = array();
+
+ // Do we want a full embed code or a link?
+ if ( $no_script || $inline || $infinite_scroll || Jetpack_AMP_Support::is_amp_request() ) {
+ return $survey_link;
+ }
+
+ if ( $type == 'iframe' ) {
+ if ( $height != 'auto' ) {
+ if ( isset( $content_width ) && is_numeric( $width ) && $width > $content_width ) {
+ $width = $content_width;
+ }
+
+ if ( ! $width ) {
+ $width = '100%';
+ } else {
+ $width = (int) $width;
+ }
+
+ if ( ! $height ) {
+ $height = '600';
+ } else {
+ $height = (int) $height;
+ }
+
+ return <<<CONTAINER
+<iframe src="{$survey_url}?iframe=1" frameborder="0" width="{$width}" height="{$height}" scrolling="auto" allowtransparency="true" marginheight="0" marginwidth="0">{$survey_link}</iframe>
+CONTAINER;
+ } elseif ( ! empty( $domain ) && ! empty( $id ) ) {
+
+ $domain = preg_replace( '/[^a-z0-9\-]/i', '', $domain );
+ $id = preg_replace( '/[\/\?&\{\}]/', '', $id );
+
+ $auto_src = esc_url( "https://{$domain}.survey.fm/{$id}" );
+ $auto_src = parse_url( $auto_src );
+
+ if ( ! is_array( $auto_src ) || count( $auto_src ) == 0 ) {
+ return '<!-- no crowdsignal output -->';
+ }
+
+ if ( ! isset( $auto_src['host'] ) || ! isset( $auto_src['path'] ) ) {
+ return '<!-- no crowdsignal output -->';
+ }
+
+ $domain = $auto_src['host'] . '/';
+ $id = ltrim( $auto_src['path'], '/' );
+
+ $settings = array(
+ 'type' => $type,
+ 'auto' => true,
+ 'domain' => $domain,
+ 'id' => $id
+ );
+ }
+ } else {
+ $text_color = preg_replace( '/[^a-f0-9]/i', '', $text_color );
+ $back_color = preg_replace( '/[^a-f0-9]/i', '', $back_color );
+
+ if (
+ ! in_array(
+ $align,
+ array(
+ 'right',
+ 'left',
+ 'top-left',
+ 'top-right',
+ 'middle-left',
+ 'middle-right',
+ 'bottom-left',
+ 'bottom-right'
+ )
+ )
+ ) {
+ $align = '';
+ }
+
+ if (
+ ! in_array(
+ $style,
+ array(
+ 'inline',
+ 'side',
+ 'corner',
+ 'rounded',
+ 'square'
+ )
+ )
+ ) {
+ $style = '';
+ }
+
+ $title = wp_strip_all_tags( $title );
+ $body = wp_strip_all_tags( $body );
+ $button = wp_strip_all_tags( $button );
+
+ $settings = array_filter( array(
+ 'title' => $title,
+ 'type' => $type,
+ 'body' => $body,
+ 'button' => $button,
+ 'text_color' => $text_color,
+ 'back_color' => $back_color,
+ 'align' => $align,
+ 'style' => $style,
+ 'id' => $survey,
+ ) );
+ }
+
+ if ( empty( $settings ) ) {
+ return '<!-- no crowdsignal output -->';
+ }
+
+ return $this->get_async_code( $settings, $survey_link );
+ }
+ } else {
+ return '<!-- no crowdsignal output -->';
+ }
+ }
+
+ function generate_scripts() {
+ $script = '';
+
+ if ( is_array( self::$scripts ) ) {
+ if ( isset( self::$scripts['rating'] ) ) {
+ $script = "<script type='text/javascript' charset='UTF-8' id='polldaddyRatings'><!--//--><![CDATA[//><!--\n";
+ foreach( self::$scripts['rating'] as $rating ) {
+ $script .= "PDRTJS_settings_{$rating['id']}{$rating['item_id']}={$rating['settings']}; if ( typeof PDRTJS_RATING !== 'undefined' ){if ( typeof PDRTJS_{$rating['id']}{$rating['item_id']} == 'undefined' ){PDRTJS_{$rating['id']}{$rating['item_id']} = new PDRTJS_RATING( PDRTJS_settings_{$rating['id']}{$rating['item_id']} );}}";
+ }
+ $script .= "\n//--><!]]></script><script type='text/javascript' charset='UTF-8' async src='https://polldaddy.com/js/rating/rating.js'></script>";
+
+ }
+
+ if ( isset( self::$scripts['poll'] ) ) {
+ foreach( self::$scripts['poll'] as $poll ) {
+ $script .= "<script type='text/javascript' charset='UTF-8' async src='{$poll['url']}'></script>";
+ }
+ }
+ }
+
+ self::$scripts = false;
+ echo $script;
+ }
+
+ /**
+ * If the theme uses infinite scroll, include jquery at the start
+ */
+ function check_infinite() {
+ if (
+ current_theme_supports( 'infinite-scroll' )
+ && class_exists( 'The_Neverending_Home_Page' )
+ && The_Neverending_Home_Page::archive_supports_infinity()
+ ) {
+ wp_enqueue_script( 'jquery' );
+ }
+ }
+
+ /**
+ * Dynamically load the .js, if needed
+ *
+ * This hooks in late (priority 11) to infinite_scroll_render to determine
+ * a posteriori if a shortcode has been called.
+ */
+ function crowdsignal_shortcode_infinite() {
+ // only try to load if a shortcode has been called and theme supports infinite scroll
+ if( self::$add_script ) {
+ $script_url = esc_url_raw( plugins_url( 'js/polldaddy-shortcode.js', __FILE__ ) );
+
+ // if the script hasn't been loaded, load it
+ // if the script loads successfully, fire an 'pd-script-load' event
+ echo <<<SCRIPT
+ <script type='text/javascript'>
+ //<![CDATA[
+ ( function( d, c, j ) {
+ if ( !d.getElementById( j ) ) {
+ var pd = d.createElement( c ), s;
+ pd.id = j;
+ pd.async = true;
+ pd.src = '{$script_url}';
+ s = d.getElementsByTagName( c )[0];
+ s.parentNode.insertBefore( pd, s );
+ } else if ( typeof jQuery !== 'undefined' ) {
+ jQuery( d.body ).trigger( 'pd-script-load' );
+ }
+ } ( document, 'script', 'pd-polldaddy-loader' ) );
+ //]]>
+ </script>
+SCRIPT;
+
+ }
+ }
+}
+
+// kick it all off
+new CrowdsignalShortcode();
+
+if ( ! function_exists( 'crowdsignal_link' ) ) {
+ // http://polldaddy.com/poll/1562975/?view=results&msg=voted
+ function crowdsignal_link( $content ) {
+ if ( Jetpack_AMP_Support::is_amp_request() ) {
+ return $content;
+ }
+
+ return preg_replace( '!(?:\n|\A)https?://(polldaddy\.com/poll|poll\.fm)/([0-9]+?)(/.*)?(?:\n|\Z)!i', "\n<script type='text/javascript' charset='utf-8' src='//static.polldaddy.com/p/$2.js'></script><noscript> <a href='https://poll.fm/$2'>View Poll</a></noscript>\n", $content );
+ }
+
+ // higher priority because we need it before auto-link and autop get to it
+ add_filter( 'the_content', 'crowdsignal_link', 1 );
+ add_filter( 'the_content_rss', 'crowdsignal_link', 1 );
+}
+
+ /**
+ * Note that Core has the oembed of '#https?://survey\.fm/.*#i' as of 5.1.
+ * This should be removed after Core has the current regex is in our minimum version.
+ *
+ * @see https://core.trac.wordpress.org/ticket/46467
+ * @todo Confirm patch landed and remove once 5.2 is the minimum version.
+ */
+wp_oembed_add_provider( '#https?://.+\.survey\.fm/.*#i', 'https://api.crowdsignal.com/oembed', true );
+
+}
diff --git a/plugins/jetpack/modules/shortcodes/css/quiz.css b/plugins/jetpack/modules/shortcodes/css/quiz.css
new file mode 100644
index 00000000..e2a0b36b
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/quiz.css
@@ -0,0 +1,56 @@
+div.jetpack-quiz {
+ border: 1px solid #deede3;
+ background-color: #f3f3f3;
+ padding: 1em;
+ line-height: 1.3em;
+ margin-bottom: 2em;
+ border-radius: .2em;
+}
+
+div.jetpack-quiz div.jetpack-quiz-question {
+ margin-bottom: .5em;
+ font-weight: bold;
+}
+
+div.jetpack-quiz div.jetpack-quiz-answer {
+ cursor: pointer;
+ margin-bottom: .5em;
+ padding: 1em 0 1em 1em;
+ border-bottom: 1px dotted #999;
+}
+div.jetpack-quiz div.jetpack-quiz-answer.last {
+ padding-bottom: 0;
+ margin-bottom: 0;
+ border-bottom: 0;
+}
+
+div.jetpack-quiz div.jetpack-quiz-answer.correct {
+ color: green;
+}
+
+div.jetpack-quiz div.jetpack-quiz-answer.wrong {
+ color: red;
+}
+
+div.jetpack-quiz div.jetpack-quiz-answer div.jetpack-quiz-explanation {
+ display: none;
+}
+
+div.jetpack-quiz div.jetpack-quiz-answer.correct div.jetpack-quiz-explanation, div.jetpack-quiz div.jetpack-quiz-answer.wrong div.jetpack-quiz-explanation {
+ display: block;
+ color: black;
+ font-size: 90%;
+ margin-top: 1em;
+}
+
+div.jetpack-quiz div.jetpack-quiz-answer.correct div.jetpack-quiz-explanation tt, div.jetpack-quiz div.jetpack-quiz-answer.wrong div.jetpack-quiz-explanation tt {
+ font-size: 85%;
+}
+
+div.jetpack-quiz pre {
+ font: 15px Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace;
+ background: transparent;
+ margin: 0;
+ padding: 0;
+}
+
diff --git a/plugins/jetpack/modules/shortcodes/css/recipes-print-rtl.css b/plugins/jetpack/modules/shortcodes/css/recipes-print-rtl.css
new file mode 100644
index 00000000..bbea4bf2
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/recipes-print-rtl.css
@@ -0,0 +1 @@
+.jetpack-recipe-meta li.jetpack-recipe-print{display:none}.jetpack-recipe-title{font-size:16pt}.jetpack-recipe-content img{display:inline-block!important;max-width:100%}.jetpack-recipe-image{display:none!important}.jetpack-recipe-content .aligncenter{display:block!important;margin:0 auto 1em!important;text-align:center!important}.jetpack-recipe-content .alignright{float:left!important;margin:0 1em .5em 0!important}.jetpack-recipe-content .alignleft{float:right!important;margin:0 0 .5em 1em!important}.jetpack-recipe-content .alignnone{display:inline-block} \ No newline at end of file
diff --git a/plugins/jetpack/modules/shortcodes/css/recipes-print-rtl.min.css b/plugins/jetpack/modules/shortcodes/css/recipes-print-rtl.min.css
new file mode 100644
index 00000000..bbea4bf2
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/recipes-print-rtl.min.css
@@ -0,0 +1 @@
+.jetpack-recipe-meta li.jetpack-recipe-print{display:none}.jetpack-recipe-title{font-size:16pt}.jetpack-recipe-content img{display:inline-block!important;max-width:100%}.jetpack-recipe-image{display:none!important}.jetpack-recipe-content .aligncenter{display:block!important;margin:0 auto 1em!important;text-align:center!important}.jetpack-recipe-content .alignright{float:left!important;margin:0 1em .5em 0!important}.jetpack-recipe-content .alignleft{float:right!important;margin:0 0 .5em 1em!important}.jetpack-recipe-content .alignnone{display:inline-block} \ No newline at end of file
diff --git a/plugins/jetpack/modules/shortcodes/css/recipes-print.css b/plugins/jetpack/modules/shortcodes/css/recipes-print.css
new file mode 100644
index 00000000..48e05179
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/recipes-print.css
@@ -0,0 +1,36 @@
+.jetpack-recipe-meta li.jetpack-recipe-print {
+ display: none;
+}
+
+.jetpack-recipe-title {
+ font-size: 16pt;
+}
+
+.jetpack-recipe-content img {
+ display: inline-block !important;
+ max-width: 100%;
+}
+
+.jetpack-recipe-image {
+ display: none !important;
+}
+
+.jetpack-recipe-content .aligncenter {
+ display: block !important;
+ margin: 0 auto 1em !important;
+ text-align: center !important;
+}
+
+.jetpack-recipe-content .alignright {
+ float: right !important;
+ margin: 0 0 .5em 1em !important;
+}
+
+.jetpack-recipe-content .alignleft {
+ float: left !important;
+ margin: 0 1em .5em 0 !important;
+}
+
+.jetpack-recipe-content .alignnone {
+ display: inline-block;
+}
diff --git a/plugins/jetpack/modules/shortcodes/css/recipes-print.min.css b/plugins/jetpack/modules/shortcodes/css/recipes-print.min.css
new file mode 100644
index 00000000..52cf81bf
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/recipes-print.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.jetpack-recipe-meta li.jetpack-recipe-print{display:none}.jetpack-recipe-title{font-size:16pt}.jetpack-recipe-content img{display:inline-block!important;max-width:100%}.jetpack-recipe-image{display:none!important}.jetpack-recipe-content .aligncenter{display:block!important;margin:0 auto 1em!important;text-align:center!important}.jetpack-recipe-content .alignright{float:right!important;margin:0 0 .5em 1em!important}.jetpack-recipe-content .alignleft{float:left!important;margin:0 1em .5em 0!important}.jetpack-recipe-content .alignnone{display:inline-block} \ No newline at end of file
diff --git a/plugins/jetpack/modules/shortcodes/css/recipes-rtl.css b/plugins/jetpack/modules/shortcodes/css/recipes-rtl.css
new file mode 100644
index 00000000..a0492b5f
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/recipes-rtl.css
@@ -0,0 +1 @@
+.jetpack-recipe{border:1px solid #f2f2f2;border-radius:1px;clear:both;margin:1.5em 1%;padding:1% 2%}.jetpack-recipe-title{border-bottom:1px solid #ccc;margin:.25em 0;padding:.25em 0}.jetpack-recipe .jetpack-recipe-meta{display:block;font-size:.9em;list-style-type:none;margin-left:0;margin-right:0;padding:0;overflow:hidden;width:100%}.jetpack-recipe .jetpack-recipe-meta li{float:right;list-style-type:none;margin:0;padding:0 0 0 5%}.jetpack-recipe-meta li.jetpack-recipe-print{float:left;padding-left:0;text-align:left}.jetpack-recipe-notes{font-style:italic} \ No newline at end of file
diff --git a/plugins/jetpack/modules/shortcodes/css/recipes-rtl.min.css b/plugins/jetpack/modules/shortcodes/css/recipes-rtl.min.css
new file mode 100644
index 00000000..a0492b5f
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/recipes-rtl.min.css
@@ -0,0 +1 @@
+.jetpack-recipe{border:1px solid #f2f2f2;border-radius:1px;clear:both;margin:1.5em 1%;padding:1% 2%}.jetpack-recipe-title{border-bottom:1px solid #ccc;margin:.25em 0;padding:.25em 0}.jetpack-recipe .jetpack-recipe-meta{display:block;font-size:.9em;list-style-type:none;margin-left:0;margin-right:0;padding:0;overflow:hidden;width:100%}.jetpack-recipe .jetpack-recipe-meta li{float:right;list-style-type:none;margin:0;padding:0 0 0 5%}.jetpack-recipe-meta li.jetpack-recipe-print{float:left;padding-left:0;text-align:left}.jetpack-recipe-notes{font-style:italic} \ No newline at end of file
diff --git a/plugins/jetpack/modules/shortcodes/css/recipes.css b/plugins/jetpack/modules/shortcodes/css/recipes.css
new file mode 100644
index 00000000..63ab2169
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/recipes.css
@@ -0,0 +1,36 @@
+.jetpack-recipe {
+ border: 1px solid #f2f2f2;
+ border-radius: 1px;
+ clear: both;
+ margin: 1.5em 1%;
+ padding: 1% 2%;
+}
+.jetpack-recipe-title {
+ border-bottom: 1px solid #ccc;
+ margin: .25em 0;
+ padding: .25em 0;
+}
+.jetpack-recipe .jetpack-recipe-meta {
+ display: block;
+ font-size: .9em;
+ list-style-type: none;
+ margin-right: 0;
+ margin-left: 0;
+ padding: 0;
+ overflow: hidden;
+ width: 100%;
+}
+.jetpack-recipe .jetpack-recipe-meta li {
+ float: left;
+ list-style-type: none;
+ margin: 0;
+ padding: 0 5% 0 0;
+}
+.jetpack-recipe-meta li.jetpack-recipe-print {
+ float: right;
+ padding-right: 0;
+ text-align: right;
+}
+.jetpack-recipe-notes {
+ font-style: italic;
+}
diff --git a/plugins/jetpack/modules/shortcodes/css/recipes.min.css b/plugins/jetpack/modules/shortcodes/css/recipes.min.css
new file mode 100644
index 00000000..e4e36a3d
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/recipes.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.jetpack-recipe{border:1px solid #f2f2f2;border-radius:1px;clear:both;margin:1.5em 1%;padding:1% 2%}.jetpack-recipe-title{border-bottom:1px solid #ccc;margin:.25em 0;padding:.25em 0}.jetpack-recipe .jetpack-recipe-meta{display:block;font-size:.9em;list-style-type:none;margin-right:0;margin-left:0;padding:0;overflow:hidden;width:100%}.jetpack-recipe .jetpack-recipe-meta li{float:left;list-style-type:none;margin:0;padding:0 5% 0 0}.jetpack-recipe-meta li.jetpack-recipe-print{float:right;padding-right:0;text-align:right}.jetpack-recipe-notes{font-style:italic} \ No newline at end of file
diff --git a/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode-rtl.css b/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode-rtl.css
new file mode 100644
index 00000000..3defeb94
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode-rtl.css
@@ -0,0 +1,145 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.slideshow-window {
+ background-color: #222;
+ border: 20px solid #222;
+ border-radius: 10px;
+ height: 0;
+ margin-bottom: 20px;
+ overflow: hidden;
+ padding-top: 30px !important;
+ padding-bottom: 56.25% !important;
+ position: relative;
+ z-index: 1;
+}
+
+.slideshow-window.slideshow-white {
+ background-color: #fff;
+ border-color: #fff;
+}
+
+.slideshow-window, .slideshow-window * {
+ box-sizing: content-box;
+}
+
+.slideshow-loading {
+ height: 100%;
+ text-align: center;
+ margin: auto;
+}
+
+body div.slideshow-window * img {
+ /* Override any styles that might be present in the page stylesheet */
+ background-color: transparent !important;
+ background-image: none !important;
+ border-width: 0 !important;
+ display: block;
+ margin: 0 auto;
+ max-width: 100%;
+ max-height: 100%;
+ padding: 0 !important;
+ position: relative;
+ transform: translateY(-50%);
+ top: 50%;
+}
+
+.slideshow-loading img {
+ vertical-align: middle;
+}
+
+.slideshow-slide {
+ display: none;
+ height: 100% !important;
+ right: 0;
+ margin: auto;
+ position: absolute;
+ text-align: center;
+ top: 0;
+ width: 100% !important;
+}
+
+.slideshow-slide img {
+ vertical-align: middle;
+}
+
+.slideshow-line-height-hack {
+ overflow: hidden;
+ width: 0px;
+ font-size: 0px;
+}
+
+.slideshow-slide-caption {
+ font-size: 13px;
+ font-family: "Helvetica Neue", sans-serif;
+ color: #f7f7f7;
+ text-shadow: #222 -1px 1px 2px;
+ line-height: 25px;
+ height: 25px;
+ position: absolute;
+ bottom: 5px;
+ right: 0;
+ z-index: 100;
+ width: 100%;
+ text-align: center;
+}
+
+.slideshow-controls {
+ z-index: 1000;
+ position: absolute;
+ bottom: 30px;
+ margin: auto;
+ text-align: center;
+ width: 100%;
+ -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
+ opacity: 0.5;
+ direction:ltr;
+ transition: 300ms opacity ease-out;
+}
+
+.slideshow-window:hover .slideshow-controls {
+ -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
+ opacity: 1;
+}
+
+body div div.slideshow-controls a,
+body div div.slideshow-controls a:hover {
+ border:2px solid rgba(255,255,255,0.1) !important;
+ background-color: #000 !important;
+ background-color: rgba(0,0,0,0.6) !important;
+ background-image: url('../img/slideshow-controls.png') !important;
+ background-repeat: no-repeat;
+ background-size: 142px 16px !important;
+ background-position: -34px 8px !important;
+ color: #222 !important;
+ margin: 0 5px !important;
+ padding: 0 !important;
+ display: inline-block !important;
+ *display: inline;
+ zoom: 1;
+ height: 32px !important;
+ width: 32px !important;
+ line-height: 32px !important;
+ text-align: center !important;
+ border-radius: 10em !important;
+ transition: 300ms border-color ease-out;
+}
+
+@media only screen and (-webkit-min-device-pixel-ratio: 1.5) {
+ body div div.slideshow-controls a,
+ body div div.slideshow-controls a:hover {
+ background-image: url('../img/slideshow-controls-2x.png') !important;
+ }
+}
+
+body div div.slideshow-controls a:hover {
+ border-color: rgba(255,255,255,1) !important;
+}
+
+body div div.slideshow-controls a:first-child { background-position: -76px 8px !important;}
+body div div.slideshow-controls a:last-child { background-position: -117px 8px !important;}
+body div div.slideshow-controls a:nth-child(2) { background-position: -34px 8px !important;}
+body div div.slideshow-controls a.running { background-position: -34px 8px !important;}
+body div div.slideshow-controls a.paused { background-position: 9px 8px !important;}
+
+.slideshow-controls a img {
+ border: 50px dotted fuchsia;
+}
diff --git a/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode-rtl.min.css b/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode-rtl.min.css
new file mode 100644
index 00000000..4342c699
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode-rtl.min.css
@@ -0,0 +1 @@
+.slideshow-window{background-color:#222;border:20px solid #222;border-radius:10px;height:0;margin-bottom:20px;overflow:hidden;padding-top:30px!important;padding-bottom:56.25%!important;position:relative;z-index:1}.slideshow-window.slideshow-white{background-color:#fff;border-color:#fff}.slideshow-window,.slideshow-window *{box-sizing:content-box}.slideshow-loading{height:100%;text-align:center;margin:auto}body div.slideshow-window * img{background-color:transparent!important;background-image:none!important;border-width:0!important;display:block;margin:0 auto;max-width:100%;max-height:100%;padding:0!important;position:relative;transform:translateY(-50%);top:50%}.slideshow-loading img{vertical-align:middle}.slideshow-slide{display:none;height:100%!important;right:0;margin:auto;position:absolute;text-align:center;top:0;width:100%!important}.slideshow-slide img{vertical-align:middle}.slideshow-line-height-hack{overflow:hidden;width:0;font-size:0}.slideshow-slide-caption{font-size:13px;font-family:"Helvetica Neue",sans-serif;color:#f7f7f7;text-shadow:#222 -1px 1px 2px;line-height:25px;height:25px;position:absolute;bottom:5px;right:0;z-index:100;width:100%;text-align:center}.slideshow-controls{z-index:1000;position:absolute;bottom:30px;margin:auto;text-align:center;width:100%;opacity:.5;direction:ltr;transition:.3s opacity ease-out}.slideshow-window:hover .slideshow-controls{opacity:1}body div div.slideshow-controls a,body div div.slideshow-controls a:hover{border:2px solid rgba(255,255,255,.1)!important;background-color:#000!important;background-color:rgba(0,0,0,.6)!important;background-image:url(../img/slideshow-controls.png)!important;background-repeat:no-repeat;background-size:142px 16px!important;background-position:-34px 8px!important;color:#222!important;margin:0 5px!important;padding:0!important;display:inline-block!important;zoom:1;height:32px!important;width:32px!important;line-height:32px!important;text-align:center!important;border-radius:10em!important;transition:.3s border-color ease-out}@media only screen and (-webkit-min-device-pixel-ratio:1.5){body div div.slideshow-controls a,body div div.slideshow-controls a:hover{background-image:url(../img/slideshow-controls-2x.png)!important}}body div div.slideshow-controls a:hover{border-color:rgba(255,255,255,1)!important}body div div.slideshow-controls a:first-child{background-position:-76px 8px!important}body div div.slideshow-controls a:last-child{background-position:-117px 8px!important}body div div.slideshow-controls a:nth-child(2){background-position:-34px 8px!important}body div div.slideshow-controls a.running{background-position:-34px 8px!important}body div div.slideshow-controls a.paused{background-position:9px 8px!important}.slideshow-controls a img{border:50px dotted #f0f} \ No newline at end of file
diff --git a/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode.css b/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode.css
new file mode 100644
index 00000000..2e416a44
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode.css
@@ -0,0 +1,157 @@
+.slideshow-window {
+ background-color: #222;
+ border: 20px solid #222;
+ border-radius: 10px;
+ height: 0;
+ margin-bottom: 20px;
+ overflow: hidden;
+ padding-top: 30px !important;
+ padding-bottom: 56.25% !important;
+ position: relative;
+ z-index: 1;
+}
+
+.slideshow-window.slideshow-white {
+ background-color: #fff;
+ border-color: #fff;
+}
+
+.slideshow-window, .slideshow-window * {
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box;
+ box-sizing: content-box;
+}
+
+.slideshow-loading {
+ height: 100%;
+ text-align: center;
+ margin: auto;
+}
+
+body div.slideshow-window * img {
+ /* Override any styles that might be present in the page stylesheet */
+ background-color: transparent !important;
+ background-image: none !important;
+ border-width: 0 !important;
+ display: block;
+ margin: 0 auto;
+ max-width: 100%;
+ max-height: 100%;
+ padding: 0 !important;
+ position: relative;
+ -webkit-transform: translateY(-50%);
+ -ms-transform: translateY(-50%);
+ transform: translateY(-50%);
+ top: 50%;
+}
+
+.slideshow-loading img {
+ vertical-align: middle;
+}
+
+.slideshow-slide {
+ display: none;
+ height: 100% !important;
+ left: 0;
+ margin: auto;
+ position: absolute;
+ text-align: center;
+ top: 0;
+ width: 100% !important;
+}
+
+.slideshow-slide img {
+ vertical-align: middle;
+}
+
+.slideshow-line-height-hack {
+ overflow: hidden;
+ width: 0px;
+ font-size: 0px;
+}
+
+.slideshow-slide-caption {
+ font-size: 13px;
+ font-family: "Helvetica Neue", sans-serif;
+ color: #f7f7f7;
+ text-shadow: #222 1px 1px 2px;
+ line-height: 25px;
+ height: 25px;
+ position: absolute;
+ bottom: 5px;
+ left: 0;
+ z-index: 100;
+ width: 100%;
+ text-align: center;
+}
+
+.slideshow-controls {
+ z-index: 1000;
+ position: absolute;
+ bottom: 30px;
+ margin: auto;
+ text-align: center;
+ width: 100%;
+ -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
+ opacity: 0.5;
+ /*rtl:ignore*/
+ direction:ltr;
+ -webkit-transition: 300ms opacity ease-out;
+ -moz-transition: 300ms opacity ease-out;
+ transition: 300ms opacity ease-out;
+}
+
+.slideshow-window:hover .slideshow-controls {
+ -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
+ opacity: 1;
+}
+
+body div div.slideshow-controls a,
+body div div.slideshow-controls a:hover {
+ border:2px solid rgba(255,255,255,0.1) !important;
+ background-color: #000 !important;
+ background-color: rgba(0,0,0,0.6) !important;
+ background-image: url('../img/slideshow-controls.png') !important;
+ background-repeat: no-repeat;
+ background-size: 142px 16px !important;
+ background-position: -34px 8px !important;
+ color: #222 !important;
+ margin: 0 5px !important;
+ padding: 0 !important;
+ display: inline-block !important;
+ *display: inline;
+ zoom: 1;
+ height: 32px !important;
+ width: 32px !important;
+ line-height: 32px !important;
+ text-align: center !important;
+ -khtml-border-radius: 10em !important;
+ -webkit-border-radius: 10em !important;
+ -moz-border-radius: 10em !important;
+ border-radius: 10em !important;
+ -webkit-transition: 300ms border-color ease-out;
+ -moz-transition: 300ms border-color ease-out;
+ -o-transition: 300ms border-color ease-out;
+ transition: 300ms border-color ease-out;
+}
+
+@media only screen and (-webkit-min-device-pixel-ratio: 1.5) {
+ body div div.slideshow-controls a,
+ body div div.slideshow-controls a:hover {
+ background-image: url('../img/slideshow-controls-2x.png') !important;
+ }
+}
+
+body div div.slideshow-controls a:hover {
+ border-color: rgba(255,255,255,1) !important;
+}
+
+body div div.slideshow-controls a:first-child { background-position: -76px 8px !important;}
+body div div.slideshow-controls a:last-child { background-position: -117px 8px !important;}
+body div div.slideshow-controls a:nth-child(2) { background-position: -34px 8px !important;}
+body div div.slideshow-controls a.running { background-position: -34px 8px !important;}
+body div div.slideshow-controls a.paused { background-position: 9px 8px !important;}
+
+.slideshow-controls a img {
+ border: 50px dotted fuchsia;
+}
diff --git a/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode.min.css b/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode.min.css
new file mode 100644
index 00000000..9642a647
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/slideshow-shortcode.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.slideshow-window{background-color:#222;border:20px solid #222;border-radius:10px;height:0;margin-bottom:20px;overflow:hidden;padding-top:30px!important;padding-bottom:56.25%!important;position:relative;z-index:1}.slideshow-window.slideshow-white{background-color:#fff;border-color:#fff}.slideshow-window,.slideshow-window *{box-sizing:content-box}.slideshow-loading{height:100%;text-align:center;margin:auto}body div.slideshow-window * img{background-color:transparent!important;background-image:none!important;border-width:0!important;display:block;margin:0 auto;max-width:100%;max-height:100%;padding:0!important;position:relative;transform:translateY(-50%);top:50%}.slideshow-loading img{vertical-align:middle}.slideshow-slide{display:none;height:100%!important;left:0;margin:auto;position:absolute;text-align:center;top:0;width:100%!important}.slideshow-slide img{vertical-align:middle}.slideshow-line-height-hack{overflow:hidden;width:0;font-size:0}.slideshow-slide-caption{font-size:13px;font-family:"Helvetica Neue",sans-serif;color:#f7f7f7;text-shadow:#222 1px 1px 2px;line-height:25px;height:25px;position:absolute;bottom:5px;left:0;z-index:100;width:100%;text-align:center}.slideshow-controls{z-index:1000;position:absolute;bottom:30px;margin:auto;text-align:center;width:100%;opacity:.5;direction:ltr;transition:.3s opacity ease-out}.slideshow-window:hover .slideshow-controls{opacity:1}body div div.slideshow-controls a,body div div.slideshow-controls a:hover{border:2px solid rgba(255,255,255,.1)!important;background-color:#000!important;background-color:rgba(0,0,0,.6)!important;background-image:url(../img/slideshow-controls.png)!important;background-repeat:no-repeat;background-size:142px 16px!important;background-position:-34px 8px!important;color:#222!important;margin:0 5px!important;padding:0!important;display:inline-block!important;zoom:1;height:32px!important;width:32px!important;line-height:32px!important;text-align:center!important;border-radius:10em!important;transition:.3s border-color ease-out}@media only screen and (-webkit-min-device-pixel-ratio:1.5){body div div.slideshow-controls a,body div div.slideshow-controls a:hover{background-image:url(../img/slideshow-controls-2x.png)!important}}body div div.slideshow-controls a:hover{border-color:rgba(255,255,255,1)!important}body div div.slideshow-controls a:first-child{background-position:-76px 8px!important}body div div.slideshow-controls a:last-child{background-position:-117px 8px!important}body div div.slideshow-controls a:nth-child(2){background-position:-34px 8px!important}body div div.slideshow-controls a.running{background-position:-34px 8px!important}body div div.slideshow-controls a.paused{background-position:9px 8px!important}.slideshow-controls a img{border:50px dotted #f0f} \ No newline at end of file
diff --git a/plugins/jetpack/modules/shortcodes/css/style.css b/plugins/jetpack/modules/shortcodes/css/style.css
new file mode 100644
index 00000000..5ef78159
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/css/style.css
@@ -0,0 +1,188 @@
+/**
+* 1. Fullscreen styles
+*/
+html.presentation-wrapper-fullscreen-parent,
+body.presentation-wrapper-fullscreen-parent {
+ overflow: hidden !important;
+}
+
+.presentation-wrapper-fullscreen-parent #wpadminbar {
+ display: none;
+}
+
+.presentation-wrapper-fullscreen,
+.presentation-wrapper-fullscreen-parent {
+ min-width: 100% !important;
+ min-height: 100% !important;
+ position: absolute !important;
+ top: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+ left: 0 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ z-index: 10000 !important;
+}
+
+.presentation-wrapper-fullscreen {
+ background-color: #808080;
+ border: none !important;
+}
+
+.presentation-wrapper-fullscreen .nav-arrow-left,
+.presentation-wrapper-fullscreen .nav-arrow-right {
+ z-index: 20001;
+}
+
+.presentation-wrapper-fullscreen .nav-fullscreen-button {
+ z-index: 20002;
+}
+
+
+/**
+ * 2. General presentation styles
+ */
+.presentation-wrapper {
+ margin: 20px auto;
+ border: 1px solid #e5e5e5;
+ overflow: hidden;
+ line-height: normal;
+}
+
+.presentation {
+ position: relative;
+ margin: 0;
+ overflow: hidden;
+ outline: none;
+}
+
+/**
+ * jmpress requires that step sizes are explicitly defined
+ * as it inserts sizeless divs before the steps. These
+ * dimensions are set by the js code on initialization
+ */
+.presentation,
+.presentation .step {
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 100% 100%;
+}
+
+/**
+ * Opacity transition durations are set by the js code
+ * so they match the presentation animation durations
+ */
+.presentation .step.fade:not(.active) {
+ opacity: 0;
+}
+
+.presentation .slide-content {
+ padding: 30px;
+}
+
+
+/**
+ * 3. Styles for the navigation arrows
+ */
+.presentation .nav-arrow-left,
+.presentation .nav-arrow-right,
+.presentation .nav-fullscreen-button {
+ position: absolute;
+ width: 34px;
+ background-repeat: no-repeat;
+ z-index: 2;
+ opacity: 0;
+
+ -webkit-transition : opacity .25s;
+ -moz-transition : opacity .25s;
+ -ms-transition : opacity .25s;
+ -o-transition : opacity .25s;
+ transition : opacity .25s;
+}
+
+.presentation .nav-arrow-left,
+.presentation .nav-arrow-right {
+ height: 100%;
+ background-image: url(../images/slide-nav.png);
+ background-size: 450% 61px;
+}
+
+.presentation .nav-arrow-left {
+ left: 0;
+ background-position: 4px 50%;
+}
+
+.presentation .nav-arrow-right {
+ right: 0;
+ background-position: -120px 50%;
+}
+
+.presentation .nav-fullscreen-button {
+ width: 32px;
+ height: 32px;
+ margin: 4px;
+ bottom: 0;
+ right: 0;
+ z-index: 3;
+ background-image: url(../images/expand.png);
+ background-size: 100% 100%;
+}
+
+.presentation:hover .nav-arrow-left,
+.presentation:hover .nav-arrow-right {
+ opacity: 1;
+}
+
+.presentation:hover .nav-fullscreen-button {
+ opacity: 0.8;
+}
+
+.presentation-wrapper-fullscreen .nav-fullscreen-button {
+ background-image: url(../images/collapse.png);
+}
+
+/**
+ * 4. Styles for the autoplay overlay
+ */
+.presentation .autoplay-overlay {
+ height: 15%;
+ width: 80%;
+ margin: 30% 10%;
+ position: relative;
+ z-index: 100;
+ display: table;
+ border-radius: 50px;
+ background-color: #e5e5e5;
+ background-color: rgba(0, 0, 0, 0.75);
+
+ -webkit-transition : opacity .5s;
+ -moz-transition : opacity .5s;
+ -ms-transition : opacity .5s;
+ -o-transition : opacity .5s;
+ transition : opacity .5s;
+}
+
+.presentation .autoplay-overlay .overlay-msg {
+ position: relative;
+ display: table-cell;
+ text-align: center;
+ vertical-align: middle;
+ color: #fff;
+}
+
+/**
+ * 5. Styles for fading steps
+ */
+.presentation .will-fade {
+ opacity: 0;
+}
+
+.presentation .do-fade {
+ opacity: 1;
+
+ -webkit-transition : opacity .5s;
+ -moz-transition : opacity .5s;
+ -ms-transition : opacity .5s;
+ -o-transition : opacity .5s;
+ transition : opacity .5s;
+}
diff --git a/plugins/jetpack/modules/shortcodes/dailymotion.php b/plugins/jetpack/modules/shortcodes/dailymotion.php
new file mode 100644
index 00000000..fd2d620f
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/dailymotion.php
@@ -0,0 +1,357 @@
+<?php
+/**
+ * Dailymotion code
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Original codes:
+ *
+ * <embed height="270" type="application/x-shockwave-flash" width="480" src="http&#58;//www.dailymotion.com/swf/video/xekmrq?additionalInfos=0" wmode="opaque" pluginspage="http&#58;//www.macromedia.com/go/getflashplayer" allowscriptaccess="never" allownetworking="internal" />
+ *
+ * <object width="480" height="240"><param name="movie" value="http://www.dailymotion.com/swf/video/xen4ms_ghinzu-cold-love-mirror-mirror_music?additionalInfos=0"></param><param name="allowFullScreen" value="true"></param><param name="allowScriptAccess" value="always"></param>
+ * <embed type="application/x-shockwave-flash" src="http://www.dailymotion.com/swf/video/xen4ms_ghinzu-cold-love-mirror-mirror_music?additionalInfos=0" width="480" height="240" allowfullscreen="true" allowscriptaccess="always"></embed>
+ * </object><br /><b><a href="http://www.dailymotion.com/video/xen4ms_ghinzu-cold-love-mirror-mirror_music">Ghinzu - Cold Love (Mirror Mirror)</a></b><br /><i>Uploaded by <a href="http://www.dailymotion.com/GhinzuTV">GhinzuTV</a>. - <a href="http://www.dailymotion.com/us/channel/music">Watch more music videos, in HD!</a></i>
+ *
+ * Code as of 01.01.11:
+ * <object width="560" height="421"><param name="movie" value="http://www.dailymotion.com/swf/video/xaose5?width=560&theme=denim&foreground=%2392ADE0&highlight=%23A2ACBF&background=%23202226&start=&animatedTitle=&iframe=0&additionalInfos=0&autoPlay=0&hideInfos=0"></param><param name="allowFullScreen" value="true"></param><param name="allowScriptAccess" value="always"></param><embed type="application/x-shockwave-flash" src="http://www.dailymotion.com/swf/video/xaose5?width=560&theme=denim&foreground=%2392ADE0&highlight=%23A2ACBF&background=%23202226&start=&animatedTitle=&iframe=0&additionalInfos=0&autoPlay=0&hideInfos=0" width="560" height="421" allowfullscreen="true" allowscriptaccess="always"></embed></object><br /><b><a href="http://www.dailymotion.com/video/x29zm17_funny-videos-of-cats-and-babies-compilation-2015_fun">Funny cats and babies!</a></b><br /><i>Uploaded by <a href="http://www.dailymotion.com/GilLavie">GilLavie</a>. - <a target="_self" href="http://www.dailymotion.com/channel/funny/featured/1">Find more funny videos.</a></i>
+ * movie param enforces anti-xss protection
+ *
+ * Scroll down for the new <iframe> embed code handler.
+ *
+ * @param string $content Post content.
+ */
+function dailymotion_embed_to_shortcode( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, 'www.dailymotion.com/swf/' ) ) {
+ return $content;
+ }
+
+ $regexp = '!<object.*>\s*(<param.*></param>\s*)*<embed((?:\s+\w+="[^"]*")*)\s+src="http(?:\:|&#0*58;)//(www\.dailymotion\.com/swf/[^"]*)"((?:\s+\w+="[^"]*")*)\s*(?:/>|>\s*</embed>)\s*</object><br /><b><a .*>.*</a></b><br /><i>.*</i>!';
+ $regexp_ent = str_replace( '&amp;#0*58;', '&amp;#0*58;|&#0*58;', htmlspecialchars( $regexp, ENT_NOQUOTES ) );
+
+ foreach ( compact( 'regexp', 'regexp_ent' ) as $reg => $regexp ) {
+ if ( ! preg_match_all( $regexp, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ $src = html_entity_decode( $match[3] );
+ $params = $match[2] . $match[4];
+
+ if ( 'regexp_ent' === $reg ) {
+ $src = html_entity_decode( $src );
+ $params = html_entity_decode( $params );
+ }
+
+ $params = wp_kses_hair( $params, array( 'http' ) );
+
+ if ( ! isset( $params['type'] ) || 'application/x-shockwave-flash' !== $params['type']['value'] ) {
+ continue;
+ }
+
+ $id = basename( substr( $src, strlen( 'www.dailymotion.com/swf' ) ) );
+ $id = preg_replace( '/[^a-z0-9].*$/i', '', $id );
+
+ $content = str_replace( $match[0], "[dailymotion id=$id]", $content );
+ /** This action is documented in modules/shortcodes/youtube.php */
+ do_action( 'jetpack_embed_to_shortcode', 'dailymotion', $id );
+ }
+ }
+ return $content;
+}
+add_filter( 'pre_kses', 'dailymotion_embed_to_shortcode' );
+
+/**
+ * DailyMotion shortcode
+ *
+ * The documented shortcode is:
+ * [dailymotion id=x8oma9]
+ *
+ * Possibilities, according to the old parsing regexp:
+ * [dailymotion x8oma9]
+ * [dailymotion=x8oma9]
+ *
+ * Hypothetical option, according to the old shortcode function is
+ * [dailymotion id=1&title=2&user=3&video=4]
+ *
+ * The new style is now:
+ * [dailymotion id=x8oma9 title=2 user=3 video=4]
+ *
+ * Supported parameters for player customization: width, height,
+ * autoplay, endscreen-enable, mute, sharing-enabled, start, subtitles-default,
+ * ui-highlight, ui-logo, ui-start-screen-info, ui-theme
+ * see https://developer.dailymotion.com/player#player-parameters
+ *
+ * @todo: Update code to sniff for iframe embeds and convert those to shortcodes.
+ *
+ * @param array $atts Shortcode attributes.
+ *
+ * @return string html
+ */
+function dailymotion_shortcode( $atts ) {
+ global $content_width;
+
+ if ( isset( $atts[0] ) ) {
+ $id = ltrim( $atts[0], '=' );
+ $atts['id'] = $id;
+
+ } else {
+ $params = shortcode_new_to_old_params( $atts );
+ parse_str( $params, $atts_new );
+
+ foreach ( $atts_new as $k => $v ) {
+ $atts[ $k ] = $v;
+ }
+ }
+
+ $atts = shortcode_atts(
+ array(
+ 'id' => '', // string.
+ 'width' => '', // int.
+ 'height' => '', // int.
+ 'title' => '', // string.
+ 'user' => '', // string.
+ 'video' => '', // string.
+ 'autoplay' => 0, // int.
+ 'endscreen-enable' => 1, // int.
+ 'mute' => 0, // int.
+ 'sharing-enable' => 1, // int.
+ 'start' => '', // int.
+ 'subtitles-default' => '', // string.
+ 'ui-highlight' => '', // string.
+ 'ui-logo' => 1, // int.
+ 'ui-start-screen-info' => 0, // int.
+ 'ui-theme' => '', // string.
+ ),
+ $atts,
+ 'dailymotion'
+ );
+
+ if ( isset( $atts['id'] ) && ! empty( $atts['id'] ) ) {
+ $id = rawurlencode( $atts['id'] );
+ } else {
+ return '<!--Dailymotion error: bad or missing ID-->';
+ }
+
+ /*set width and height using provided parameters if any */
+ $width = isset( $atts['width'] ) ? intval( $atts['width'] ) : 0;
+ $height = isset( $atts['height'] ) ? intval( $atts['height'] ) : 0;
+
+ if ( ! $width && ! $height ) {
+ if ( ! empty( $content_width ) ) {
+ $width = absint( $content_width );
+ } else {
+ $width = 425;
+ }
+ $height = $width / 425 * 334;
+ } elseif ( ! $height ) {
+ $height = $width / 425 * 334;
+ } elseif ( ! $width ) {
+ $width = $height / 334 * 425;
+ }
+
+ /**
+ * Let's add parameters if needed.
+ *
+ * @see https://developer.dailymotion.com/player
+ */
+ $player_params = array();
+
+ if ( isset( $atts['autoplay'] ) && '1' === $atts['autoplay'] ) {
+ $player_params['autoplay'] = '1';
+ }
+ if ( isset( $atts['endscreen-enable'] ) && '0' === $atts['endscreen-enable'] ) {
+ $player_params['endscreen-enable'] = '0';
+ }
+ if ( isset( $atts['mute'] ) && '1' === $atts['mute'] ) {
+ $player_params['mute'] = '1';
+ }
+ if ( isset( $atts['sharing-enable'] ) && '0' === $atts['sharing-enable'] ) {
+ $player_params['sharing-enable'] = '0';
+ }
+ if ( isset( $atts['start'] ) && ! empty( $atts['start'] ) ) {
+ $player_params['start'] = abs( intval( $atts['start'] ) );
+ }
+ if ( isset( $atts['subtitles-default'] ) && ! empty( $atts['subtitles-default'] ) ) {
+ $player_params['subtitles-default'] = esc_attr( $atts['subtitles-default'] );
+ }
+ if ( isset( $atts['ui-highlight'] ) && ! empty( $atts['ui-highlight'] ) ) {
+ $player_params['ui-highlight'] = esc_attr( $atts['ui-highlight'] );
+ }
+ if ( isset( $atts['ui-logo'] ) && '0' === $atts['ui-logo'] ) {
+ $player_params['ui-logo'] = '0';
+ }
+ if ( isset( $atts['ui-start-screen-info'] ) && '0' === $atts['ui-start-screen-info'] ) {
+ $player_params['ui-start-screen-info'] = '0';
+ }
+ if ( isset( $atts['ui-theme'] ) && in_array( strtolower( $atts['ui-theme'] ), array( 'dark', 'light' ), true ) ) {
+ $player_params['ui-theme'] = esc_attr( $atts['ui-theme'] );
+ }
+
+ // Add those parameters to the Video URL.
+ $video_url = add_query_arg(
+ $player_params,
+ 'https://www.dailymotion.com/embed/video/' . $id
+ );
+
+ $output = '';
+
+ if ( preg_match( '/^[A-Za-z0-9]+$/', $id ) ) {
+ $output .= '<iframe width="' . esc_attr( $width ) . '" height="' . esc_attr( $height ) . '" src="' . esc_url( $video_url ) . '" style="border:0;" allowfullscreen></iframe>';
+
+ $video = preg_replace( '/[^-a-z0-9_]/i', '', $atts['video'] );
+ $title = wp_kses( $atts['title'], array() );
+ if (
+ array_key_exists( 'video', $atts )
+ && $video
+ && array_key_exists( 'title', $atts )
+ && $title
+ ) {
+ $output .= '<br /><strong><a href="' . esc_url( 'http://www.dailymotion.com/video/' . $video ) . '" target="_blank">' . esc_html( $title ) . '</a></strong>';
+ }
+
+ $user = preg_replace( '/[^-a-z0-9_]/i', '', $atts['user'] );
+ if ( array_key_exists( 'user', $atts ) && $user ) {
+ /* translators: %s is a Dailymotion user name */
+ $output .= '<br /><em>' . wp_kses(
+ sprintf(
+ /* Translators: placeholder is a Dailymotion username, linking to a Dailymotion profile page. */
+ __( 'Uploaded by %s', 'jetpack' ),
+ '<a href="' . esc_url( 'http://www.dailymotion.com/' . $user ) . '" target="_blank">' . esc_html( $user ) . '</a>'
+ ),
+ array(
+ 'a' => array(
+ 'href' => true,
+ 'target' => true,
+ ),
+ )
+ ) . '</em>';
+ }
+ }
+
+ return $output;
+}
+add_shortcode( 'dailymotion', 'dailymotion_shortcode' );
+
+/**
+ * DailyMotion Channel Shortcode
+ *
+ * Examples:
+ * [dailymotion-channel user=MatthewDominick]
+ * [dailymotion-channel user=MatthewDominick type=grid] (supports grid, carousel, badge/default)
+ *
+ * @param array $atts Shortcode attributes.
+ */
+function dailymotion_channel_shortcode( $atts ) {
+ $username = $atts['user'];
+
+ switch ( $atts['type'] ) {
+ case 'grid':
+ $channel_iframe = '<iframe width="300px" height="264px" scrolling="no" style="border:0;" src="' . esc_url( '//www.dailymotion.com/badge/user/' . $username . '?type=grid' ) . '"></iframe>';
+ break;
+ case 'carousel':
+ $channel_iframe = '<iframe width="300px" height="360px" scrolling="no" style="border:0;" src="' . esc_url( '//www.dailymotion.com/badge/user/' . $username . '?type=carousel' ) . '"></iframe>';
+ break;
+ default:
+ $channel_iframe = '<iframe width="300px" height="78px" scrolling="no" style="border:0;" src="' . esc_url( '//www.dailymotion.com/badge/user/' . $username ) . '"></iframe>';
+ }
+
+ return $channel_iframe;
+}
+add_shortcode( 'dailymotion-channel', 'dailymotion_channel_shortcode' );
+
+/**
+ * Embed Reversal for Badge/Channel
+ *
+ * @param string $content Post content.
+ */
+function dailymotion_channel_reversal( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, 'dailymotion.com/badge/' ) ) {
+ return $content;
+ }
+
+ /*
+ * Sample embed code:
+ * <iframe width="300px" height="360px" scrolling="no" frameborder="0" src="http://www.dailymotion.com/badge/user/Dailymotion?type=carousel"></iframe>
+ */
+
+ $regexes = array();
+
+ $regexes[] = '#<iframe[^>]+?src=" (?:https?:)?//(?:www\.)?dailymotion\.com/badge/user/([^"\'/]++) "[^>]*+></iframe>#ix';
+
+ // Let's play nice with the visual editor too.
+ $regexes[] = '#&lt;iframe(?:[^&]|&(?!gt;))+?src=" (?:https?:)?//(?:www\.)?dailymotion\.com/badge/user/([^"\'/]++) "(?:[^&]|&(?!gt;))*+&gt;&lt;/iframe&gt;#ix';
+
+ foreach ( $regexes as $regex ) {
+ if ( ! preg_match_all( $regex, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ $url_pieces = wp_parse_url( $match[1] );
+
+ if ( 'type=carousel' === $url_pieces['query'] ) {
+ $type = 'carousel';
+ } elseif ( 'type=grid' === $url_pieces['query'] ) {
+ $type = 'grid';
+ } else {
+ $type = 'badge';
+ }
+
+ $shortcode = '[dailymotion-channel user=' . esc_attr( $url_pieces['path'] ) . ' type=' . esc_attr( $type ) . ']';
+ $replace_regex = sprintf( '#\s*%s\s*#', preg_quote( $match[0], '#' ) );
+ $content = preg_replace( $replace_regex, sprintf( "\n\n%s\n\n", $shortcode ), $content );
+ }
+ }
+
+ return $content;
+}
+add_filter( 'pre_kses', 'dailymotion_channel_reversal' );
+
+/**
+ * Dailymotion Embed Reversal (with new iframe code as of 17.09.2014)
+ *
+ * Converts a generic HTML embed code from Dailymotion into an
+ * oEmbeddable URL.
+ *
+ * @param string $content Post content.
+ */
+function jetpack_dailymotion_embed_reversal( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, 'dailymotion.com/embed' ) ) {
+ return $content;
+ }
+
+ /*
+ * Sample embed code as of Sep 17th 2014:
+ * <iframe frameborder="0" width="480" height="270" src="//www.dailymotion.com/embed/video/x25x71x" allowfullscreen></iframe><br /><a href="http://www.dailymotion.com/video/x25x71x_dog-with-legs-in-casts-learns-how-to-enter-the-front-door_animals" target="_blank">Dog with legs in casts learns how to enter the...</a> <i>by <a href="http://www.dailymotion.com/videobash" target="_blank">videobash</a></i>
+ */
+ $regexes = array();
+
+ // I'm Konstantin and I love regex.
+ $regexes[] = '#<iframe[^>]+?src=" (?:https?:)?//(?:www\.)?dailymotion\.com/embed/video/([^"\'/]++) "[^>]*+>\s*+</iframe>\s*+(?:<br\s*+/>)?\s*+
+ (?: <a[^>]+?href=" (?:https?:)?//(?:www\.)?dailymotion\.com/[^"\']++ "[^>]*+>.+?</a>\s*+ )?
+ (?: <i>.*?<a[^>]+?href=" (?:https?:)?//(?:www\.)?dailymotion\.com/[^"\']++ "[^>]*+>.+?</a>\s*+</i> )?#ix';
+
+ $regexes[] = '#&lt;iframe(?:[^&]|&(?!gt;))+?src=" (?:https?:)?//(?:www\.)?dailymotion\.com/embed/video/([^"\'/]++) "(?:[^&]|&(?!gt;))*+&gt;\s*+&lt;/iframe&gt;\s*+(?:&lt;br\s*+/&gt;)?\s*+
+ (?: &lt;a(?:[^&]|&(?!gt;))+?href=" (?:https?:)?//(?:www\.)?dailymotion\.com/[^"\']++ "(?:[^&]|&(?!gt;))*+&gt;.+?&lt;/a&gt;\s*+ )?
+ (?: &lt;i&gt;.*?&lt;a(?:[^&]|&(?!gt;))+?href=" (?:https?:)?//(?:www\.)?dailymotion\.com/[^"\']++ "(?:[^&]|&(?!gt;))*+&gt;.+?&lt;/a&gt;\s*+&lt;/i&gt; )?#ix';
+
+ foreach ( $regexes as $regex ) {
+ if ( ! preg_match_all( $regex, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ $url = esc_url( sprintf( 'https://dailymotion.com/video/%s', $match[1] ) );
+ $replace_regex = sprintf( '#\s*%s\s*#', preg_quote( $match[0], '#' ) );
+ $content = preg_replace( $replace_regex, sprintf( "\n\n%s\n\n", $url ), $content );
+
+ /** This action is documented in modules/shortcodes/youtube.php */
+ do_action( 'jetpack_embed_to_shortcode', 'dailymotion', $url );
+ }
+ }
+
+ return $content;
+}
+add_filter( 'pre_kses', 'jetpack_dailymotion_embed_reversal' );
diff --git a/plugins/jetpack/modules/shortcodes/facebook.php b/plugins/jetpack/modules/shortcodes/facebook.php
new file mode 100644
index 00000000..e2b11e3f
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/facebook.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Facebook embeds
+ *
+ * @package Jetpack
+ */
+
+define( 'JETPACK_FACEBOOK_EMBED_REGEX', '#^https?://(www.)?facebook\.com/([^/]+)/(posts|photos)/([^/]+)?#' );
+define( 'JETPACK_FACEBOOK_ALTERNATE_EMBED_REGEX', '#^https?://(www.)?facebook\.com/permalink.php\?([^\s]+)#' );
+define( 'JETPACK_FACEBOOK_PHOTO_EMBED_REGEX', '#^https?://(www.)?facebook\.com/photo.php\?([^\s]+)#' );
+define( 'JETPACK_FACEBOOK_PHOTO_ALTERNATE_EMBED_REGEX', '#^https?://(www.)?facebook\.com/([^/]+)/photos/([^/]+)?#' );
+define( 'JETPACK_FACEBOOK_VIDEO_EMBED_REGEX', '#^https?://(www.)?facebook\.com/video.php\?([^\s]+)#' );
+define( 'JETPACK_FACEBOOK_VIDEO_ALTERNATE_EMBED_REGEX', '#^https?://(www.)?facebook\.com/([^/]+)/videos/([^/]+)?#' );
+
+
+/*
+ * Example URL: https://www.facebook.com/VenusWilliams/posts/10151647007373076
+ */
+wp_embed_register_handler( 'facebook', JETPACK_FACEBOOK_EMBED_REGEX, 'jetpack_facebook_embed_handler' );
+
+/*
+ * Example URL: https://www.facebook.com/permalink.php?id=222622504529111&story_fbid=559431180743788
+ */
+wp_embed_register_handler( 'facebook-alternate', JETPACK_FACEBOOK_ALTERNATE_EMBED_REGEX, 'jetpack_facebook_embed_handler' );
+
+/*
+ * Photos are handled on a different endpoint; e.g. https://www.facebook.com/photo.php?fbid=10151609960150073&set=a.398410140072.163165.106666030072&type=1
+ */
+wp_embed_register_handler( 'facebook-photo', JETPACK_FACEBOOK_PHOTO_EMBED_REGEX, 'jetpack_facebook_embed_handler' );
+
+/*
+ * Photos (from pages for example) can be at
+ */
+wp_embed_register_handler( 'facebook-alternate-photo', JETPACK_FACEBOOK_PHOTO_ALTERNATE_EMBED_REGEX, 'jetpack_facebook_embed_handler' );
+
+/*
+ * Videos e.g. https://www.facebook.com/video.php?v=772471122790796
+ */
+wp_embed_register_handler( 'facebook-video', JETPACK_FACEBOOK_VIDEO_EMBED_REGEX, 'jetpack_facebook_embed_handler' );
+
+/*
+ * Videos https://www.facebook.com/WhiteHouse/videos/10153398464269238/
+ */
+wp_embed_register_handler( 'facebook-alternate-video', JETPACK_FACEBOOK_VIDEO_ALTERNATE_EMBED_REGEX, 'jetpack_facebook_embed_handler' );
+
+/**
+ * Callback to modify output of embedded Facebook posts.
+ *
+ * @param array $matches Regex partial matches against the URL passed.
+ * @param array $attr Attributes received in embed response.
+ * @param array $url Requested URL to be embedded.
+ */
+function jetpack_facebook_embed_handler( $matches, $attr, $url ) {
+ if ( false !== strpos( $url, 'video.php' ) || false !== strpos( $url, '/videos/' ) ) {
+ $embed = sprintf( '<div class="fb-video" data-allowfullscreen="true" data-href="%s"></div>', esc_url( $url ) );
+ } else {
+ $width = 552; // As of 01/2017, the default width of Facebook embeds when no width attribute provided.
+
+ global $content_width;
+ if ( isset( $content_width ) ) {
+ $width = min( $width, $content_width );
+ }
+
+ $embed = sprintf( '<fb:post href="%s" data-width="%s"></fb:post>', esc_url( $url ), esc_attr( $width ) );
+ }
+
+ // since Facebook is a faux embed, we need to load the JS SDK in the wpview embed iframe.
+ if (
+ defined( 'DOING_AJAX' )
+ && DOING_AJAX
+ // No need to check for a nonce here, that's already handled by Core further up.
+ && ! empty( $_POST['action'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ && 'parse-embed' === $_POST['action'] // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ ) {
+ ob_start();
+ wp_scripts()->do_items( array( 'jetpack-facebook-embed' ) );
+ $scripts = ob_get_clean();
+ return $embed . $scripts;
+ } else {
+ wp_enqueue_script( 'jetpack-facebook-embed' );
+ return $embed;
+ }
+}
+
+/**
+ * Shortcode handler.
+ *
+ * @param array $atts Shortcode attributes.
+ */
+function jetpack_facebook_shortcode_handler( $atts ) {
+ global $wp_embed;
+
+ if ( empty( $atts['url'] ) ) {
+ return;
+ }
+
+ if ( ! preg_match( JETPACK_FACEBOOK_EMBED_REGEX, $atts['url'] )
+ && ! preg_match( JETPACK_FACEBOOK_PHOTO_EMBED_REGEX, $atts['url'] )
+ && ! preg_match( JETPACK_FACEBOOK_VIDEO_EMBED_REGEX, $atts['url'] )
+ && ! preg_match( JETPACK_FACEBOOK_VIDEO_ALTERNATE_EMBED_REGEX, $atts['url'] ) ) {
+ return;
+ }
+
+ return $wp_embed->shortcode( $atts, $atts['url'] );
+}
+add_shortcode( 'facebook', 'jetpack_facebook_shortcode_handler' );
diff --git a/plugins/jetpack/modules/shortcodes/flatio.php b/plugins/jetpack/modules/shortcodes/flatio.php
new file mode 100644
index 00000000..3b5c31f8
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/flatio.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Flat.io embed
+ *
+ * Example URL: https://flat.io/score/5a5268ed41396318cbd7772c-string-quartet-for-rainy-days
+ *
+ * @package Jetpack
+ */
+
+// Register oEmbed provider.
+wp_oembed_add_provider( 'https://flat.io/score/*', 'https://flat.io/services/oembed', false );
+wp_oembed_add_provider( 'https://*.flat.io/score/*', 'https://flat.io/services/oembed', false );
diff --git a/plugins/jetpack/modules/shortcodes/flickr.php b/plugins/jetpack/modules/shortcodes/flickr.php
new file mode 100644
index 00000000..73db1172
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/flickr.php
@@ -0,0 +1,239 @@
+<?php
+/**
+ * Flickr Short Code
+ * Author: kellan
+ * License: BSD/GPL/public domain (take your pick)
+ *
+ * [flickr video=http://www.flickr.com/photos/chaddles/2402990826]
+ * [flickr video=2402990826]
+ * [flickr video=2402990826 show_info=no]
+ * [flickr video=2402990826 w=200 h=150]
+ * [flickr video=2402990826 secret=846d9c1b39]
+ *
+ * @package Jetpack
+ */
+
+/*
+ * <object type="application/x-shockwave-flash" width="400" height="300" data="http://www.flickr.com/apps/video/stewart.swf?v=71377" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="flashvars" value="intl_lang=en-us&photo_secret=846d9c1be9&photo_id=2345938910"></param> <param name="movie" value="http://www.flickr.com/apps/video/stewart.swf?v=71377"></param> <param name="bgcolor" value="#000000"></param> <param name="allowFullScreen" value="true"></param><embed type="application/x-shockwave-flash" src="http://www.flickr.com/apps/video/stewart.swf?v=71377" bgcolor="#000000" allowfullscreen="true" flashvars="intl_lang=en-us&photo_secret=846d9c1be9&photo_id=2345938910" height="300" width="400"></embed></object>
+ */
+
+/**
+ * Transform embed to shortcode on save.
+ *
+ * @param string $content Post content.
+ */
+function flickr_embed_to_shortcode( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, '/www.flickr.com/apps/video/stewart.swf' ) ) {
+ return $content;
+ }
+
+ $regexp = '%(<object.*?(?:<(?!/?(?:object|embed)\s+).*?)*?)?<embed((?:\s+\w+="[^"]*")*)\s+src="http(?:\:|&#0*58;)//www.flickr.com/apps/video/stewart.swf[^"]*"((?:\s+\w+="[^"]*")*)\s*(?:/>|>\s*</embed>)(?(1)\s*</object>)%';
+ $regexp_ent = str_replace(
+ array(
+ '&amp;#0*58;',
+ '[^&gt;]*',
+ '[^&lt;]*',
+ ),
+ array(
+ '&amp;#0*58;|&#0*58;',
+ '[^&]*(?:&(?!gt;)[^&]*)*',
+ '[^&]*(?:&(?!lt;)[^&]*)*',
+ ),
+ htmlspecialchars( $regexp, ENT_NOQUOTES )
+ );
+
+ foreach ( compact( 'regexp', 'regexp_ent' ) as $reg => $regexp ) {
+ if ( ! preg_match_all( $regexp, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+ foreach ( $matches as $match ) {
+ $params = $match[2] . $match[3];
+
+ if ( 'regexp_ent' === $reg ) {
+ $params = html_entity_decode( $params );
+ }
+
+ $params = wp_kses_hair( $params, array( 'http' ) );
+ if (
+ ! isset( $params['type'] )
+ || 'application/x-shockwave-flash' !== $params['type']['value']
+ || ! isset( $params['flashvars'] )
+ ) {
+ continue;
+ }
+
+ $flashvars = array();
+ wp_parse_str( html_entity_decode( $params['flashvars']['value'] ), $flashvars );
+
+ if ( ! isset( $flashvars['photo_id'] ) ) {
+ continue;
+ }
+
+ $code_atts = array( 'video' => $flashvars['photo_id'] );
+
+ if (
+ isset( $flashvars['flickr_show_info_box'] )
+ && 'true' === $flashvars['flickr_show_info_box']
+ ) {
+ $code_atts['show_info'] = 'true';
+ }
+
+ if ( ! empty( $flashvars['photo_secret'] ) ) {
+ $code_atts['secret'] = $flashvars['photo_secret'];
+ }
+
+ if ( ! empty( $params['width']['value'] ) ) {
+ $code_atts['w'] = (int) $params['width']['value'];
+ }
+
+ if ( ! empty( $params['height']['value'] ) ) {
+ $code_atts['h'] = (int) $params['height']['value'];
+ }
+
+ $code = '[flickr';
+ foreach ( $code_atts as $k => $v ) {
+ $code .= " $k=$v";
+ }
+ $code .= ']';
+
+ $content = str_replace( $match[0], $code, $content );
+ /** This action is documented in modules/shortcodes/youtube.php */
+ do_action( 'jetpack_embed_to_shortcode', 'flickr_video', $flashvars['photo_id'] );
+ }
+ }
+
+ return $content;
+}
+add_filter( 'pre_kses', 'flickr_embed_to_shortcode' );
+
+/**
+ * Flickr Shortcode handler.
+ *
+ * @param array $atts Shortcode attributes.
+ */
+function flickr_shortcode_handler( $atts ) {
+ $atts = shortcode_atts(
+ array(
+ 'video' => 0,
+ 'photo' => 0,
+ 'show_info' => 0,
+ 'w' => 400,
+ 'h' => 300,
+ 'secret' => 0,
+ ),
+ $atts,
+ 'flickr'
+ );
+
+ if ( ! empty( $atts['video'] ) ) {
+ $showing = 'video';
+ $src = $atts['video'];
+ } elseif ( ! empty( $atts['photo'] ) ) {
+ $showing = 'photo';
+ $src = $atts['photo'];
+ } else {
+ return '';
+ }
+
+ if ( is_ssl() ) {
+ $src = str_replace( 'http://', 'https://', $src );
+ }
+
+ if ( 'video' === $showing ) {
+
+ if ( ! is_numeric( $src ) && ! preg_match( '~^(https?:)?//([\da-z\-]+\.)*?((static)?flickr\.com|flic\.kr)/.*~i', $src ) ) {
+ return '';
+ }
+
+ if ( preg_match( '!photos/(([0-9a-zA-Z-_]+)|([0-9]+@N[0-9]+))/([0-9]+)/?$!', $src, $m ) ) {
+ $atts['photo_id'] = $m[4];
+ } else {
+ $atts['photo_id'] = $atts['video'];
+ }
+
+ if (
+ ! isset( $atts['show_info'] )
+ || in_array( $atts['show_info'], array( 'yes', 'true' ), true )
+ ) {
+ $atts['show_info'] = 'true';
+ } elseif ( in_array( $atts['show_info'], array( 'false', 'no' ), true ) ) {
+ $atts['show_info'] = 'false';
+ }
+
+ if ( isset( $atts['secret'] ) ) {
+ $atts['secret'] = preg_replace( '![^\w]+!i', '', $atts['secret'] );
+ }
+
+ return flickr_shortcode_video_markup( $atts );
+ } elseif ( 'photo' === $showing ) {
+
+ if ( ! preg_match( '~^(https?:)?//([\da-z\-]+\.)*?((static)?flickr\.com|flic\.kr)/.*~i', $src ) ) {
+ return '';
+ }
+
+ $src = sprintf( '%s/player/', untrailingslashit( $src ) );
+
+ return sprintf( '<iframe src="%s" height="%s" width="%s" frameborder="0" allowfullscreen webkitallowfullscreen mozallowfullscreen oallowfullscreen msallowfullscreen></iframe>', esc_url( $src ), esc_attr( $atts['h'] ), esc_attr( $atts['w'] ) );
+ }
+
+ return false;
+}
+
+/**
+ * Return HTML markup for a Flickr embed.
+ *
+ * @param array $atts Shortcode attributes.
+ */
+function flickr_shortcode_video_markup( $atts ) {
+ $atts = array_map( 'esc_attr', $atts );
+ $http = ( is_ssl() ) ? 'https://' : 'http://';
+
+ $photo_vars = "photo_id=$atts[photo_id]";
+ if ( isset( $atts['secret'] ) ) {
+ $photo_vars .= "&amp;photo_secret=$atts[secret]";
+ }
+
+ return <<<EOD
+<object type="application/x-shockwave-flash" width="$atts[w]" height="$atts[h]" data="{$http}www.flickr.com/apps/video/stewart.swf?v=1.161" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="flashvars" value="$photo_vars&amp;flickr_show_info_box=$atts[show_info]"></param><param name="movie" value="{$http}www.flickr.com/apps/video/stewart.swf?v=1.161"></param><param name="bgcolor" value="#000000"></param><param name="allowFullScreen" value="true"></param><param name="wmode" value="opaque"></param><embed type="application/x-shockwave-flash" src="{$http}www.flickr.com/apps/video/stewart.swf?v=1.161" bgcolor="#000000" allowfullscreen="true" flashvars="$photo_vars&amp;flickr_show_info_box=$atts[show_info]" wmode="opaque" height="$atts[h]" width="$atts[w]"></embed></object>
+EOD;
+}
+
+add_shortcode( 'flickr', 'flickr_shortcode_handler' );
+
+// Override core's Flickr support because Flickr oEmbed doesn't support web embeds.
+wp_embed_register_handler( 'flickr', '#https?://(www\.)?flickr\.com/.*#i', 'jetpack_flickr_oembed_handler' );
+
+/**
+ * Callback to modify output of embedded Vimeo video using Jetpack's shortcode.
+ *
+ * @since 3.9
+ *
+ * @param array $matches Regex partial matches against the URL passed.
+ * @param array $attr Attributes received in embed response.
+ * @param array $url Requested URL to be embedded.
+ *
+ * @return string Return output of Vimeo shortcode with the proper markup.
+ */
+function jetpack_flickr_oembed_handler( $matches, $attr, $url ) {
+ /*
+ * Legacy slideshow embeds end with /show/
+ * e.g. http://www.flickr.com/photos/yarnaholic/sets/72157615194738969/show/
+ */
+ if ( '/show/' !== substr( $url, -strlen( '/show/' ) ) ) {
+ // These lookups need cached, as they don't use WP_Embed (which caches).
+ $cache_key = md5( $url . wp_json_encode( $attr ) );
+ $cache_group = 'oembed_flickr';
+
+ $html = wp_cache_get( $cache_key, $cache_group );
+
+ if ( false === $html ) {
+ $html = _wp_oembed_get_object()->get_html( $url, $attr );
+
+ wp_cache_set( $cache_key, $html, $cache_group, 60 * MINUTE_IN_SECONDS );
+ }
+
+ return $html;
+ }
+
+ return flickr_shortcode_handler( array( 'photo' => $url ) );
+}
diff --git a/plugins/jetpack/modules/shortcodes/getty.php b/plugins/jetpack/modules/shortcodes/getty.php
new file mode 100644
index 00000000..d473e726
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/getty.php
@@ -0,0 +1,222 @@
+<?php
+/**
+ * Getty shortcode
+ *
+ * [getty src="82278805" width="$width" height="$height"]
+ * <div class="getty embed image" style="background-color:#fff;display:inline-block;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#a7a7a7;font-size:11px;width:100%;max-width:462px;"><div style="padding:0;margin:0;text-align:left;"><a href="http://www.gettyimages.com/detail/82278805" target="_blank" style="color:#a7a7a7;text-decoration:none;font-weight:normal !important;border:none;display:inline-block;">Embed from Getty Images</a></div><div style="overflow:hidden;position:relative;height:0;padding:80.086580% 0 0 0;width:100%;"><iframe src="//embed.gettyimages.com/embed/82278805?et=jGiu6FXXSpJDGf1SnwLV2g&sig=TFVNFtqghwNw5iJQ1MFWnI8f4Y40_sfogfZLhai6SfA=" width="462" height="370" scrolling="no" frameborder="0" style="display:inline-block;position:absolute;top:0;left:0;width:100%;height:100%;"></iframe></div><p style="margin:0;"></p></div>
+ *
+ * @package Jetpack
+ */
+
+if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ add_action( 'init', 'jetpack_getty_enable_embeds' );
+} else {
+ jetpack_getty_enable_embeds();
+}
+
+/**
+ * Register Getty as oembed provider. Add filter to reverse iframes to shortcode. Register [getty] shortcode.
+ *
+ * @since 4.5.0
+ * @since 5.8.0 removed string parameter.
+ */
+function jetpack_getty_enable_embeds() {
+
+ // Support their oEmbed Endpoint.
+ wp_oembed_add_provider( '#https?://www\.gettyimages\.com/detail/.*#i', 'https://embed.gettyimages.com/oembed/', true );
+ wp_oembed_add_provider( '#https?://(www\.)?gty\.im/.*#i', 'https://embed.gettyimages.com/oembed/', true );
+
+ // Allow iframes to be filtered to short code (so direct copy+paste can be done).
+ add_filter( 'pre_kses', 'wpcom_shortcodereverse_getty' );
+
+ // Actually display the Getty Embed.
+ add_shortcode( 'getty', 'jetpack_getty_shortcode' );
+}
+
+/**
+ * Filters the oEmbed provider URL for Getty URLs to include site URL host as
+ * caller if available, falling back to "wordpress.com". Must be applied at
+ * time of embed in case that `init` is too early (WP.com REST API).
+ *
+ * @module shortcodes
+ *
+ * @since 5.8.0
+ *
+ * @see WP_oEmbed::fetch
+ *
+ * @return string oEmbed provider URL
+ */
+add_filter( 'oembed_fetch_url', 'getty_add_oembed_endpoint_caller' );
+
+/**
+ * Filter the embeds to add a caller parameter.
+ *
+ * @param string $provider URL of the oEmbed provider.
+ */
+function getty_add_oembed_endpoint_caller( $provider ) {
+ // By time filter is called, original provider URL has had url, maxwidth,
+ // maxheight query parameters added.
+ if ( 0 !== strpos( $provider, 'https://embed.gettyimages.com/oembed/' ) ) {
+ return $provider;
+ }
+
+ // Set the caller argument to pass to Getty's oembed provider.
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+
+ // Only include caller for non-private sites.
+ if ( ! function_exists( 'is_private_blog' ) || ! is_private_blog() ) {
+ $host = wp_parse_url( get_bloginfo( 'url' ), PHP_URL_HOST );
+ }
+
+ // Fall back to WordPress.com.
+ if ( empty( $host ) ) {
+ $host = 'wordpress.com';
+ }
+ } else {
+ $host = wp_parse_url( get_home_url(), PHP_URL_HOST );
+ }
+
+ return add_query_arg( 'caller', $host, $provider );
+}
+
+/**
+ * Compose shortcode based on Getty iframes.
+ *
+ * @since 4.5.0
+ *
+ * @param string $content Post content.
+ *
+ * @return mixed
+ */
+function wpcom_shortcodereverse_getty( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, '.gettyimages.com/' ) ) {
+ return $content;
+ }
+
+ $regexp = '!<iframe\s+src=[\'"](https?:)?//embed\.gettyimages\.com/embed(/|/?\?assets=)([a-z0-9_-]+(,[a-z0-9_-]+)*)[^\'"]*?[\'"]((?:\s+\w+=[\'"][^\'"]*[\'"])*)((?:[\s\w]*))></iframe>!i';
+ $regexp_ent = str_replace( '&amp;#0*58;', '&amp;#0*58;|&#0*58;', htmlspecialchars( $regexp, ENT_NOQUOTES ) );
+
+ // Markup pattern for 2017 embed syntax with significant differences from the prior pattern.
+ $regexp_2017 = '!<a.+?class=\'gie-(single|slideshow)\'.+?gie\.widgets\.load\({([^}]+)}\).+?embed-cdn\.gettyimages\.com/widgets\.js.+?</script>!';
+ $regexp_2017_ent = str_replace( '&amp;#0*58;', '&amp;#0*58;|&#0*58;', htmlspecialchars( $regexp_2017, ENT_NOQUOTES ) );
+
+ foreach ( compact( 'regexp_2017', 'regexp_2017_ent', 'regexp', 'regexp_ent' ) as $reg => $regexp ) {
+ if ( ! preg_match_all( $regexp, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ if ( 'regexp_2017' === $reg || 'regexp_2017_ent' === $reg ) {
+ // Extract individual keys from the matched JavaScript object.
+ $params = $match[2];
+ if ( ! preg_match_all( '!(?P<key>\w+)\s*:\s*([\'"](?P<value>[^\'"]*?)(px)?[\'"])!', $params, $key_matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $key_matches as $key_match ) {
+ switch ( $key_match['key'] ) {
+ case 'items':
+ $ids = $key_match['value'];
+ break;
+ case 'w':
+ $width = (int) $key_match['value'];
+ break;
+ case 'h':
+ $height = (int) $key_match['value'];
+ break;
+ case 'tld':
+ $tld = $key_match['value'];
+ break;
+ }
+ }
+ } else {
+ $params = $match[5];
+ if ( 'regexp_ent' === $reg ) {
+ $params = html_entity_decode( $params );
+ }
+ $params = wp_kses_hair( $params, array( 'http' ) );
+
+ $ids = esc_html( $match[3] );
+ $width = isset( $params['width'] ) ? (int) $params['width']['value'] : 0;
+ $height = isset( $params['height'] ) ? (int) $params['height']['value'] : 0;
+ }
+
+ if ( empty( $ids ) ) {
+ continue;
+ }
+
+ $shortcode = '[getty src="' . esc_attr( $ids ) . '"';
+ if ( ! empty( $width ) ) {
+ $shortcode .= ' width="' . esc_attr( $width ) . '"';
+ }
+ if ( ! empty( $height ) ) {
+ $shortcode .= ' height="' . esc_attr( $height ) . '"';
+ }
+ /*
+ * While it does not appear to have any practical impact, Getty has
+ * requested that we include TLD in the embed request
+ */
+ if ( ! empty( $tld ) ) {
+ $shortcode .= ' tld="' . esc_attr( $tld ) . '"';
+ }
+ $shortcode .= ']';
+
+ $content = str_replace( $match[0], $shortcode, $content );
+ }
+ }
+
+ // strip out enclosing div and any other markup.
+ $regexp = '%<div class="getty\s[^>]*+>.*?<div[^>]*+>(\[getty[^\]]*+\])\s*</div>.*?</div>%is';
+ $regexp_ent = str_replace( array( '&amp;#0*58;', '[^&gt;]' ), array( '&amp;#0*58;|&#0*58;', '[^&]' ), htmlspecialchars( $regexp, ENT_NOQUOTES ) );
+
+ foreach ( compact( 'regexp', 'regexp_ent' ) as $reg => $regexp ) {
+ if ( ! preg_match_all( $regexp, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ $content = str_replace( $match[0], $match[1], $content );
+ }
+ }
+
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extras', 'html_to_shortcode', 'getty' );
+
+ return $content;
+}
+
+/**
+ * Parse shortcode arguments and render its output.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode parameters.
+ * @param string $content Content enclosed by shortcode tags.
+ *
+ * @return string
+ */
+function jetpack_getty_shortcode( $atts, $content = '' ) {
+
+ if ( ! empty( $content ) ) {
+ $src = $content;
+ } elseif ( ! empty( $atts['src'] ) ) {
+ $src = $atts['src'];
+ } elseif ( ! empty( $atts[0] ) ) {
+ $src = $atts[0];
+ } else {
+ return '<!-- Missing Getty Source ID -->';
+ }
+
+ $src = preg_replace( '/^([\da-z-]+(,[\da-z-]+)*).*$/', '$1', $src );
+
+ $params = array(
+ 'width' => isset( $atts['width'] ) ? (int) $atts['width'] : null,
+ 'height' => isset( $atts['height'] ) ? (int) $atts['height'] : null,
+ );
+
+ if ( ! empty( $atts['tld'] ) ) {
+ $params['tld'] = $atts['tld'];
+ }
+
+ return wp_oembed_get( 'https://gty.im/' . $src, array_filter( $params ) );
+}
diff --git a/plugins/jetpack/modules/shortcodes/gist.php b/plugins/jetpack/modules/shortcodes/gist.php
new file mode 100644
index 00000000..eba0a1a3
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/gist.php
@@ -0,0 +1,224 @@
+<?php
+/**
+ * GitHub's Gist site supports oEmbed but their oembed provider only
+ * returns raw HTML (no styling) and the first little bit of the code.
+ *
+ * Their JavaScript-based embed method is a lot better, so that's what we're using.
+ *
+ * Supported formats:
+ * Full URL: https://gist.github.com/57cc50246aab776e110060926a2face2
+ * Full URL with username: https://gist.github.com/jeherve/57cc50246aab776e110060926a2face2
+ * Full URL linking to specific file: https://gist.github.com/jeherve/57cc50246aab776e110060926a2face2#file-wp-config-php
+ * Full URL, no username, linking to specific file: https://gist.github.com/57cc50246aab776e110060926a2face2#file-wp-config-php
+ * Gist ID: [gist]57cc50246aab776e110060926a2face2[/gist]
+ * Gist ID within tag: [gist 57cc50246aab776e110060926a2face2]
+ * Gist ID with username: [gist jeherve/57cc50246aab776e110060926a2face2]
+ * Gist private ID with username: [gist xknown/fc5891af153e2cf365c9]
+ *
+ * @package Jetpack
+ */
+
+wp_embed_register_handler( 'github-gist', '#https?://gist\.github\.com/([a-zA-Z0-9/]+)(\#file\-[a-zA-Z0-9\_\-]+)?#', 'github_gist_embed_handler' );
+add_shortcode( 'gist', 'github_gist_shortcode' );
+
+/**
+ * Handle gist embeds.
+ *
+ * @since 2.8.0
+ *
+ * @global WP_Embed $wp_embed
+ *
+ * @param array $matches Results after parsing the URL using the regex in wp_embed_register_handler().
+ * @param array $attr Embed attributes.
+ * @param string $url The original URL that was matched by the regex.
+ * @param array $rawattr The original unmodified attributes.
+ * @return string The embed HTML.
+ */
+function github_gist_embed_handler( $matches, $attr, $url, $rawattr ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ // Let the shortcode callback do all the work.
+ return github_gist_shortcode( $matches, $url );
+}
+
+/**
+ * Extract an ID from a Gist shortcode or a full Gist URL.
+ *
+ * @since 7.3.0
+ *
+ * @param string $gist Gist shortcode or full Gist URL.
+ *
+ * @return array $gist_info {
+ * Array of information about our gist.
+ * @type string $id Unique identifier for the gist.
+ * @type string $file File name if the gist links to a specific file.
+ * }
+ */
+function jetpack_gist_get_shortcode_id( $gist = '' ) {
+ $gist_info = array(
+ 'id' => '',
+ 'file' => '',
+ );
+ // Simple shortcode, with just an ID.
+ if ( ctype_alnum( $gist ) ) {
+ $gist_info['id'] = $gist;
+ }
+
+ // Full URL? Only keep the relevant parts.
+ $parsed_url = wp_parse_url( $gist );
+ if (
+ ! empty( $parsed_url )
+ && is_array( $parsed_url )
+ && isset( $parsed_url['scheme'], $parsed_url['host'], $parsed_url['path'] )
+ ) {
+ // Not a Gist URL? Bail.
+ if ( 'gist.github.com' !== $parsed_url['host'] ) {
+ return array(
+ 'id' => '',
+ 'file' => '',
+ );
+ }
+
+ // Keep the file name if there was one.
+ if ( ! empty( $parsed_url['fragment'] ) ) {
+ $gist_info['file'] = preg_replace( '/(?:file-)(.+)/', '$1', $parsed_url['fragment'] );
+ }
+
+ // Keep the unique identifier without any leading or trailing slashes.
+ if ( ! empty( $parsed_url['path'] ) ) {
+ $gist_info['id'] = preg_replace( '/^\/([^\.]+)\./', '$1', $parsed_url['path'] );
+ // Overwrite $gist with our identifier to clean it up below.
+ $gist = $gist_info['id'];
+ }
+ }
+
+ // Not a URL nor an ID? Look for "username/id", "/username/id", or "id", and only keep the ID.
+ if ( preg_match( '#^/?(([a-z0-9_-]+/)?([a-z0-9]+))$#i', $gist, $matches ) ) {
+ $gist_info['id'] = $matches[3];
+ }
+
+ return $gist_info;
+}
+
+/**
+ * Callback for gist shortcode.
+ *
+ * @since 2.8.0
+ *
+ * @param array $atts Attributes found in the shortcode.
+ * @param string $content Content enclosed by the shortcode.
+ *
+ * @return string The gist HTML.
+ */
+function github_gist_shortcode( $atts, $content = '' ) {
+
+ if ( empty( $atts[0] ) && empty( $content ) ) {
+ if ( current_user_can( 'edit_posts' ) ) {
+ return esc_html__( 'Please specify a Gist URL or ID.', 'jetpack' );
+ } else {
+ return '<!-- Missing Gist ID -->';
+ }
+ }
+
+ $id = ( ! empty( $content ) ) ? $content : $atts[0];
+
+ // Parse a URL to get an ID we can use.
+ $gist_info = jetpack_gist_get_shortcode_id( $id );
+ if ( empty( $gist_info['id'] ) ) {
+ if ( current_user_can( 'edit_posts' ) ) {
+ return esc_html__( 'The Gist ID you provided is not valid. Please try a different one.', 'jetpack' );
+ } else {
+ return '<!-- Invalid Gist ID -->';
+ }
+ } else {
+ // Add trailing .json to all unique gist identifiers.
+ $id = $gist_info['id'] . '.json';
+ }
+
+ // The file name can come from the URL passed, or from a shortcode attribute.
+ if ( ! empty( $gist_info['file'] ) ) {
+ $file = $gist_info['file'];
+ } elseif ( ! empty( $atts['file'] ) ) {
+ $file = $atts['file'];
+ } else {
+ $file = '';
+ }
+
+ // Replace - by . to get a real file name from slug.
+ if ( ! empty( $file ) ) {
+ // Find the last -.
+ $dash_position = strrpos( $file, '-' );
+ if ( false !== $dash_position ) {
+ // Replace the - by a period.
+ $file = substr_replace( $file, '.', $dash_position, 1 );
+ }
+
+ $file = rawurlencode( $file );
+ }
+
+ if (
+ class_exists( 'Jetpack_AMP_Support' )
+ && Jetpack_AMP_Support::is_amp_request()
+ ) {
+ /*
+ * According to <https://www.ampproject.org/docs/reference/components/amp-gist#height-(required)>:
+ *
+ * > Note: You must find the height of the gist by inspecting it with your browser (e.g., Chrome Developer Tools).
+ *
+ * However, this does not seem to be the case any longer. The actual height of the content does get set in the
+ * page after loading. So this is just the initial height.
+ * See <https://github.com/ampproject/amphtml/pull/17738>.
+ */
+ $height = 240;
+
+ $amp_tag = sprintf(
+ '<amp-gist layout="fixed-height" data-gistid="%s" height="%s"',
+ esc_attr( basename( $id, '.json' ) ),
+ esc_attr( $height )
+ );
+ if ( ! empty( $file ) ) {
+ $amp_tag .= sprintf( ' data-file="%s"', esc_attr( $file ) );
+ }
+ $amp_tag .= '></amp-gist>';
+ return $amp_tag;
+ }
+
+ // URL points to the entire gist, including the file name if there was one.
+ $id = ( ! empty( $file ) ? $id . '?file=' . $file : $id );
+
+ wp_enqueue_script(
+ 'jetpack-gist-embed',
+ Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/gist.min.js', 'modules/shortcodes/js/gist.js' ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ true
+ );
+
+ // inline style to prevent the bottom margin to the embed that themes like TwentyTen, et al., add to tables.
+ $return = '<style>.gist table { margin-bottom: 0; }</style><div class="gist-oembed" data-gist="' . esc_attr( $id ) . '"></div>';
+
+ if (
+ // No need to check for a nonce here, that's already handled by Core further up.
+ // phpcs:disable WordPress.Security.NonceVerification.Missing
+ isset( $_POST['type'] )
+ && 'embed' === $_POST['type']
+ && isset( $_POST['action'] )
+ && 'parse-embed' === $_POST['action']
+ // phpcs:enable WordPress.Security.NonceVerification.Missing
+ ) {
+ return github_gist_simple_embed( $id );
+ }
+
+ return $return;
+}
+
+/**
+ * Use script tag to load shortcode in editor.
+ * Can't use wp_enqueue_script here.
+ *
+ * @since 3.9.0
+ *
+ * @param string $id The ID of the gist.
+ */
+function github_gist_simple_embed( $id ) {
+ $id = str_replace( 'json', 'js', $id );
+ return '<script src="' . esc_url( "https://gist.github.com/$id" ) . '"></script>'; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
+}
diff --git a/plugins/jetpack/modules/shortcodes/googleapps.php b/plugins/jetpack/modules/shortcodes/googleapps.php
new file mode 100644
index 00000000..55c4580e
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/googleapps.php
@@ -0,0 +1,255 @@
+<?php
+/**
+ * Google Docs and Google Calendar Shortcode
+ *
+ * Presentation:
+ * <iframe src="https://docs.google.com/present/embed?id=dhfhrphh_123drp8s65c&interval=15&autoStart=true&loop=true&size=l" frameborder="0" width="700" height="559"></iframe>
+ * <iframe src="https://docs.google.com/presentation/embed?id=13ItX4jV0SOSdr-ZjHarcpTh9Lr4omfsHAp87jpxv8-0&start=false&loop=false&delayms=3000" frameborder="0" width="960" height="749" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe>
+ *
+ * Document:
+ * <iframe src="https://docs.google.com/document/pub?id=1kDatklacdZ_tZUOpWtt_ONzY97Ldj2zFcuO9LBY2Ln4&amp;embedded=true"></iframe>
+ * <iframe src="https://docs.google.com/document/d/1kDatklacdZ_tZUOpWtt_ONzY97Ldj2zFcuO9LBY2Ln4/pub?embedded=true"></iframe>
+ * <iframe src="https://docs.google.com/document/d/e/2PACX-1vRkpIdasKL-eKXDjJgpEONduUspZTz0YmKaajfie0eJYnzikuyusuG1_V8X8T9XflN9l8A1oCM2sgEA/pub?embedded=true"></iframe>
+ *
+ * External document:
+ * <iframe width=100% height=560px frameborder=0 src=https://docs.google.com/a/pranab.in/viewer?a=v&pid=explorer&chrome=false&embedded=true&srcid=1VTMwdgGiDMt8MCr75-YkQP-4u9WmEp1Qvf6C26KYBgFilxU2qndpd-VHhBIn&hl=en></iframe>
+ *
+ * Spreadsheet Form:
+ * <iframe src="https://spreadsheets.google.com/embeddedform?formkey=dEVOYnMzZG5jMUpGbjFMYjFYNVB3NkE6MQ" width="760" height="710" frameborder="0" marginheight="0" marginwidth="0">Loading...</iframe>
+ *
+ * Spreadsheet Widget:
+ * <iframe width='500' height='300' frameborder='0' src='https://spreadsheets1.google.com/a/petedavies.com/pub?hl=en&hl=en&key=0AjSij7nlnXvKdHNsNjRSWG12YmVfOEFwdlMxQ3J1S1E&single=true&gid=0&output=html&widget=true'></iframe>
+ * <iframe width='500' height='300' frameborder='0' src='https://spreadsheets.google.com/spreadsheet/pub?hl=en&hl=en&key=0AhInIwfvYrIUdGJiTXhtUEhBSFVPUzdRZU5OMDlqdnc&output=html&widget=true'></iframe>
+ *
+ * Calendar:
+ * <iframe src="https://www.google.com/calendar/embed?src=serjant%40gmail.com&ctz=Europe/Sofia" style="border: 0" width="800" height="600" frameborder="0" scrolling="no"></iframe>
+ * <iframe src="http://www.google.com/calendar/hosted/belcastro.com/embed?src=n8nr8sd6v9hnus3nmlk7ed1238%40group.calendar.google.com&ctz=Europe/Zurich" style="border: 0" width="800" height="600" frameborder="0" scrolling="no"></iframe>
+ *
+ * Customized calendar:
+ * <iframe src="https://www.google.com/calendar/embed?title=asdf&amp;showTitle=0&amp;showNav=0&amp;showDate=0&amp;showPrint=0&amp;showTabs=0&amp;showCalendars=0&amp;
+ * showTz=0&amp;mode=AGENDA&amp;height=300&amp;wkst=2&amp;hl=fi&amp;bgcolor=%23ffcccc&amp;src=m52gdmbgelo3itf00u1v44g0ns%40group.calendar.google.com&amp;color=%234E5D6C&amp;
+ * src=serjant%40gmail.com&amp;color=%235229A3&amp;ctz=Europe%2FRiga" style=" border:solid 1px #777 " width="500" height="300" frameborder="0" scrolling="no"></iframe>
+ *
+ * Generic
+ * <iframe src="https://docs.google.com/file/d/0B0SIdZW7iu-zX1RWREJpMXVHZVU/preview" width="640" height="480"></iframe>
+ *
+ * @package Jetpack
+ */
+
+add_filter( 'pre_kses', 'googleapps_embed_to_shortcode' );
+add_shortcode( 'googleapps', 'googleapps_shortcode' );
+
+/**
+ * Reverse iframe embed to shortcode mapping HTML attributes to shortcode attributes.
+ *
+ * @since 4.5.0
+ *
+ * @param string $content Post content.
+ *
+ * @return mixed
+ */
+function googleapps_embed_to_shortcode( $content ) {
+ if (
+ ! is_string( $content )
+ || false === stripos( $content, '<iframe' )
+ && false === stripos( $content, '.google.com' )
+ ) {
+ return $content;
+ }
+
+ $regexp = '#<iframe((?:\s+\w+="[^"]*")*?)\s*src="https?://(docs|drive|spreadsheets\d*|calendar|www)*\.google\.com/(?!maps)([-\w\./]+)(?:\?)?([^"]+)?"\s*((?:\s+\w+="[^"]*")*?)>.*?</iframe>#i';
+ $regexp_ent = str_replace( '&amp;#0*58;', '&amp;#0*58;|&#0*58;', htmlspecialchars( $regexp, ENT_NOQUOTES ) );
+ $regexp_squot = str_replace( '"', "'", $regexp );
+ $regexp_ent_squot = str_replace( '"', "'", $regexp_ent );
+ $regexp_noquot = '!<iframe(.*?)src=https://(docs|drive)\.google\.com/[-\.\w/]*?(viewer)\?(.*?)>(.*?)</iframe>!';
+ $regexp_ent_noquot = str_replace( '&amp;#0*58;', '&amp;#0*58;|&#0*58;', htmlspecialchars( $regexp_noquot, ENT_NOQUOTES ) );
+
+ foreach ( compact( 'regexp', 'regexp_ent', 'regexp_squot', 'regexp_ent_squot', 'regexp_noquot', 'regexp_ent_noquot' ) as $reg => $regexp ) {
+ if ( ! preg_match_all( $regexp, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ $params = $match[1] . $match[5];
+ if ( in_array( $reg, array( 'regexp_ent', 'regexp_ent_squot' ), true ) ) {
+ $params = html_entity_decode( $params );
+ }
+
+ $params = wp_kses_hair( $params, array( 'http' ) );
+
+ $width = 0;
+ $height = 0;
+
+ if ( isset( $params['width'] ) ) {
+ $width = (int) $params['width']['value'];
+ }
+
+ if ( isset( $params['height'] ) ) {
+ $height = (int) $params['height']['value'];
+ }
+
+ // allow the user to specify width greater than 200 inside text widgets.
+ if (
+ $width > 400
+ // We don't need to check a nonce here. A nonce is already checked "further up" in most code paths.
+ // In the case where no nonce is ever checked, setting this $_POST parameter doesn't do anything the submitter couldn't already do (set the width/height).
+ && isset( $_POST['widget-text'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ ) {
+ $width = 200;
+ $height = 200;
+ }
+
+ $attributes = '';
+ if ( isset( $params['width'] ) && '100%' === $params['width']['value'] ) {
+ $width = '100%';
+ }
+
+ if ( $width ) {
+ $attributes = ' width="' . $width . '"';
+ }
+
+ if ( $height ) {
+ $attributes .= ' height="' . $height . '"';
+ }
+
+ $domain = 'spreadsheets';
+ if ( in_array( $match[2], array( 'docs', 'drive', 'www', 'calendar' ), true ) ) {
+ $domain = $match[2];
+ }
+
+ // Make sure this is actually something that the shortcode supports. If it's not, leave the HTML alone.
+ if ( ! googleapps_validate_domain_and_dir( $domain, $match[3] ) ) {
+ continue;
+ }
+
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extras', 'html_to_shortcode', googleapps_service_name( $domain, $match[3] ) );
+
+ $content = str_replace( $match[0], '[googleapps domain="' . $domain . '" dir="' . $match[3] . '" query="' . esc_attr( $match[4] ) . '"' . $attributes . ' /]', $content );
+ }
+ }
+
+ return $content;
+}
+
+/**
+ * Parse shortcode attributes and output a Google Docs embed.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode attributes.
+ *
+ * @return string
+ */
+function googleapps_shortcode( $atts ) {
+ global $content_width;
+
+ $attr = shortcode_atts(
+ array(
+ 'width' => '100%',
+ 'height' => '560',
+ 'domain' => 'docs',
+ 'dir' => 'document',
+ 'query' => '',
+ 'src' => '',
+ ),
+ $atts
+ );
+
+ if ( isset( $content_width ) && is_numeric( $attr['width'] ) && $attr['width'] > $content_width ) {
+ $attr['width'] = $content_width;
+ }
+
+ if ( isset( $content_width ) && '560' === $attr['height'] ) {
+ $attr['height'] = floor( $content_width * 3 / 4 );
+ }
+
+ if ( isset( $atts[0] ) && $atts[0] ) {
+ $attr['src'] = $atts[0];
+ }
+
+ if ( $attr['src'] && preg_match( '!https?://(docs|drive|spreadsheets\d*|calendar|www)*\.google\.com/([-\w\./]+)\?([^"]+)!', $attr['src'], $matches ) ) {
+ $attr['domain'] = $matches[1];
+ $attr['dir'] = $matches[2];
+ parse_str( htmlspecialchars_decode( $matches[3] ), $query_ar );
+ $query_ar['chrome'] = 'false';
+ $query_ar['embedded'] = 'true';
+ $attr['query'] = http_build_query( $query_ar );
+ }
+
+ if ( ! googleapps_validate_domain_and_dir( $attr['domain'], $attr['dir'] ) ) {
+ return '<!-- Unsupported URL -->';
+ }
+
+ $attr['query'] = $attr['dir'] . '?' . $attr['query'];
+
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extras', 'embeds', googleapps_service_name( $attr['domain'], $attr['dir'] ) );
+
+ return sprintf(
+ '<iframe src="%s" frameborder="0" width="%s" height="%s" marginheight="0" marginwidth="0" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe>',
+ esc_url( 'https://' . $attr['domain'] . '.google.com/' . $attr['query'] ),
+ esc_attr( $attr['width'] ),
+ esc_attr( $attr['height'] )
+ );
+}
+
+/**
+ * Check that the domain blogs to a Google Apps domain.
+ *
+ * @since 4.5.0
+ *
+ * @param string $domain Google subdomain.
+ * @param string $dir Subdirectory of the shared URL.
+ *
+ * @return bool
+ */
+function googleapps_validate_domain_and_dir( $domain, $dir ) {
+ if ( ! in_array( $domain, array( 'docs', 'drive', 'www', 'spreadsheets', 'calendar' ), true ) ) {
+ return false;
+ }
+
+ // Calendars.
+ if ( ( 'www' === $domain || 'calendar' === $domain ) && 'calendar/' !== substr( $dir, 0, 9 ) ) {
+ return false;
+ }
+
+ // Docs.
+ if ( in_array( $domain, array( 'docs', 'drive' ), true ) && ! preg_match( '![-\.\w/]*(presentation/embed|presentation/d/(.*)|present/embed|document/pub|spreadsheets/d/(.*)|document/d/(e/)?[\w-]+/pub|file/d/[\w-]+/preview|viewer|forms/d/(.*)/viewform|spreadsheet/\w+)$!', $dir ) ) {
+ return false;
+ }
+
+ // Spreadsheets.
+ if ( 'spreadsheets' === $domain && ! preg_match( '!^([-\.\w/]+/pub|[-\.\w/]*embeddedform)$!', $dir ) ) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Get the name of the service we'll be embedding.
+ *
+ * @since 4.5.0
+ *
+ * @param string $domain Google subdomain.
+ * @param string $dir Subdirectory of the shared URL.
+ *
+ * @return string
+ */
+function googleapps_service_name( $domain, $dir ) {
+ switch ( $domain ) {
+ case 'drive':
+ case 'docs':
+ $service_name = ( 'present/embed' === $dir ) ? 'googledocs_presentation' : 'googledocs_document';
+ break;
+ case 'spreadsheets':
+ $service_name = ( 'embeddedform' === $dir ) ? 'googledocs_form' : 'googledocs_spreadsheet';
+ break;
+ case 'calendar':
+ default:
+ $service_name = 'google_calendar';
+ }
+
+ return $service_name;
+}
diff --git a/plugins/jetpack/modules/shortcodes/googlemaps.php b/plugins/jetpack/modules/shortcodes/googlemaps.php
new file mode 100644
index 00000000..435e8da0
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/googlemaps.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Google Maps embeds.
+ *
+ * Supported formats:
+ * <iframe width="425" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://maps.google.com/maps?f=q&amp;source=s_q&amp;hl=bg&amp;geocode=&amp;q=%D0%9C%D0%BB%D0%B0%D0%B4%D0%BE%D1%81%D1%82+1,+%D0%A1%D0%BE%D1%84%D0%B8%D1%8F,+%D0%91%D1%8A%D0%BB%D0%B3%D0%B0%D1%80%D0%B8%D1%8F&amp;sll=37.0625,-95.677068&amp;sspn=40.545434,79.013672&amp;ie=UTF8&amp;hq=&amp;hnear=%D0%9C%D0%BB%D0%B0%D0%B4%D0%BE%D1%81%D1%82+1&amp;ll=42.654446,23.372061&amp;spn=0.036864,0.077162&amp;t=h&amp;z=14&amp;output=embed"></iframe><br /><small><a href="http://maps.google.com/maps?f=q&amp;source=embed&amp;hl=bg&amp;geocode=&amp;q=%D0%9C%D0%BB%D0%B0%D0%B4%D0%BE%D1%81%D1%82+1,+%D0%A1%D0%BE%D1%84%D0%B8%D1%8F,+%D0%91%D1%8A%D0%BB%D0%B3%D0%B0%D1%80%D0%B8%D1%8F&amp;sll=37.0625,-95.677068&amp;sspn=40.545434,79.013672&amp;ie=UTF8&amp;hq=&amp;hnear=%D0%9C%D0%BB%D0%B0%D0%B4%D0%BE%D1%81%D1%82+1&amp;ll=42.654446,23.372061&amp;spn=0.036864,0.077162&amp;t=h&amp;z=14" style="color:#0000FF;text-align:left">Вижте по-голяма карта</a></small>
+ * [googlemaps https://maps.google.com/maps?f=q&hl=en&geocode=&q=San+Francisco,+CA&sll=43.469466,-83.998504&sspn=0.01115,0.025942&g=San+Francisco,+CA&ie=UTF8&z=12&iwloc=addr&ll=37.808156,-122.402458&output=embed&s=AARTsJp56EajYksz3JXgNCwT3LJnGsqqAQ&w=425&h=350]
+ * [googlemaps https://mapsengine.google.com/map/embed?mid=zbBhkou4wwtE.kUmp8K6QJ7SA&w=640&h=480]
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Google maps iframe - transforms code that looks like that:
+ * <iframe width="425" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://maps.google.com/maps?f=q&amp;source=s_q&amp;hl=bg&amp;geocode=&amp;q=%D0%9C%D0%BB%D0%B0%D0%B4%D0%BE%D1%81%D1%82+1,+%D0%A1%D0%BE%D1%84%D0%B8%D1%8F,+%D0%91%D1%8A%D0%BB%D0%B3%D0%B0%D1%80%D0%B8%D1%8F&amp;sll=37.0625,-95.677068&amp;sspn=40.545434,79.013672&amp;ie=UTF8&amp;hq=&amp;hnear=%D0%9C%D0%BB%D0%B0%D0%B4%D0%BE%D1%81%D1%82+1&amp;ll=42.654446,23.372061&amp;spn=0.036864,0.077162&amp;t=h&amp;z=14&amp;output=embed"></iframe><br /><small><a href="http://maps.google.com/maps?f=q&amp;source=embed&amp;hl=bg&amp;geocode=&amp;q=%D0%9C%D0%BB%D0%B0%D0%B4%D0%BE%D1%81%D1%82+1,+%D0%A1%D0%BE%D1%84%D0%B8%D1%8F,+%D0%91%D1%8A%D0%BB%D0%B3%D0%B0%D1%80%D0%B8%D1%8F&amp;sll=37.0625,-95.677068&amp;sspn=40.545434,79.013672&amp;ie=UTF8&amp;hq=&amp;hnear=%D0%9C%D0%BB%D0%B0%D0%B4%D0%BE%D1%81%D1%82+1&amp;ll=42.654446,23.372061&amp;spn=0.036864,0.077162&amp;t=h&amp;z=14" style="color:#0000FF;text-align:left">Вижте по-голяма карта</a></small>
+ * into the [googlemaps http://...] shortcode format
+ *
+ * @param string $content Post content.
+ */
+function jetpack_googlemaps_embed_to_short_code( $content ) {
+
+ if ( ! is_string( $content ) || ( false === strpos( $content, 'maps.google.' ) && 1 !== preg_match( '@google\.[^/]+/maps?@', $content ) ) ) {
+ return $content;
+ }
+
+ /*
+ * IE and TinyMCE format things differently
+ * &lt;iframe width="425" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="<a href="https://maps.google.co.uk/maps/ms?msa=0&amp;amp;msid=206216869547772496318.0004bf5f0ff25aea47bd9&amp;amp;hl=en&amp;amp;ie=UTF8&amp;amp;t=m&amp;amp;ll=50.91917,-1.398808&amp;amp;spn=0.013225,0.011794&amp;amp;output=embed&quot;&gt;&lt;/iframe&gt;&lt;br">https://maps.google.co.uk/maps/ms?msa=0&amp;amp;msid=206216869547772496318.0004bf5f0ff25aea47bd9&amp;amp;hl=en&amp;amp;ie=UTF8&amp;amp;t=m&amp;amp;ll=50.91917,-1.398808&amp;amp;spn=0.013225,0.011794&amp;amp;output=embed"&gt;&lt;/iframe&gt;&lt;br</a> /&gt;&lt;small&gt;View &lt;a href="<a href="https://maps.google.co.uk/maps/ms?msa=0&amp;amp;msid=206216869547772496318.0004bf5f0ff25aea47bd9&amp;amp;hl=en&amp;amp;ie=UTF8&amp;amp;t=m&amp;amp;ll=50.91917,-1.398808&amp;amp;spn=0.013225,0.011794&amp;amp;source=embed">https://maps.google.co.uk/maps/ms?msa=0&amp;amp;msid=206216869547772496318.0004bf5f0ff25aea47bd9&amp;amp;hl=en&amp;amp;ie=UTF8&amp;amp;t=m&amp;amp;ll=50.91917,-1.398808&amp;amp;spn=0.013225,0.011794&amp;amp;source=embed</a>" style="color:#0000FF;text-align:left"&gt;OARA Membership Discount Map&lt;/a&gt; in a larger map&lt;/small&gt;
+ */
+ if ( strpos( $content, 'src="<a href="' ) !== false ) {
+ $content = preg_replace_callback( '#&lt;iframe\s[^&]*?(?:&(?!gt;)[^&]*?)*?src="<a href="https?://(.*)?\.google\.(.*?)/(.*?)\?(.+?)&quot;[^&]*?(?:&(?!gt;)[^&]*?)*?&gt;\s*&lt;/iframe&gt;&lt;br">[^"]*?"&gt;\s*&lt;/iframe&gt;(?:&lt;br</a>\s*/&gt;\s*&lt;small&gt;.*?&lt;/small&gt;)?#i', 'jetpack_googlemaps_embed_to_short_code_callback', $content );
+ return $content;
+ }
+
+ $content = preg_replace_callback( '!\<iframe\s[^>]*?src="https?://(.*)?\.google\.(.*?)/(.*?)\?(.+?)"[^>]*?\>\s*\</iframe\>(?:\s*(?:\<br\s*/?\>)?\s*\<small\>.*?\</small\>)?!i', 'jetpack_googlemaps_embed_to_short_code_callback', $content );
+
+ $content = preg_replace_callback( '#&lt;iframe\s[^&]*?(?:&(?!gt;)[^&]*?)*?src="https?://(.*)?\.google\.(.*?)/(.*?)\?(.+?)"[^&]*?(?:&(?!gt;)[^&]*?)*?&gt;\s*&lt;/iframe&gt;(?:\s*(?:&lt;br\s*/?&gt;)?\s*&lt;small&gt;.*?&lt;/small&gt;)?#i', 'jetpack_googlemaps_embed_to_short_code_callback', $content );
+
+ return $content;
+}
+
+/**
+ * Callback transforming a Google Maps iFrame code into a shortcode.
+ *
+ * @param array $match Array of embed parameters used to build the final URL.
+ */
+function jetpack_googlemaps_embed_to_short_code_callback( $match ) {
+
+ if ( preg_match( '/\bwidth=[\'"](\d+)(%)?/', $match[0], $width ) ) {
+ $percent = ! empty( $width[2] ) ? '%' : '';
+ $width = absint( $width[1] ) . $percent;
+ } else {
+ $width = 425;
+ }
+
+ if ( preg_match( '/\bheight=[\'"](\d+)(%)?/', $match[0], $height ) ) {
+ $percent = ! empty( $height[2] ) ? '%' : '';
+ $height = absint( $height[1] ) . $percent;
+ } else {
+ $height = 350;
+ }
+
+ $url = "https://{$match[1]}.google.{$match[2]}/{$match[3]}?{$match[4]}&amp;w={$width}&amp;h={$height}";
+
+ /** This action is documented in modules/shortcodes/youtube.php */
+ do_action( 'jetpack_embed_to_shortcode', 'googlemaps', $url );
+
+ return "[googlemaps $url]";
+}
+
+add_filter( 'pre_kses', 'jetpack_googlemaps_embed_to_short_code' );
+
+/**
+ * Display the [googlemaps] shortcode
+ *
+ * @param array $atts Shortcode attributes.
+ */
+function jetpack_googlemaps_shortcode( $atts ) {
+ if ( ! isset( $atts[0] ) ) {
+ return '';
+ }
+
+ $params = ltrim( $atts[0], '=' );
+
+ $width = 425;
+ $height = 350;
+
+ if ( preg_match( '!^https?://(www|maps|mapsengine)\.google(\.co|\.com)?(\.[a-z]+)?/.*?(\?.+)!i', $params, $match ) ) {
+ $params = str_replace( '&amp;amp;', '&amp;', $params );
+ $params = str_replace( '&amp;', '&', $params );
+ parse_str( $params, $arg );
+
+ if ( isset( $arg['hq'] ) ) {
+ unset( $arg['hq'] );
+ }
+
+ $url = '';
+ foreach ( (array) $arg as $key => $value ) {
+ if ( 'w' === $key ) {
+ $percent = ( '%' === substr( $value, -1 ) ) ? '%' : '';
+ $width = (int) $value . $percent;
+ } elseif ( 'h' === $key ) {
+ $height = (int) $value;
+ } else {
+ $key = str_replace( '_', '.', $key );
+ $url .= esc_attr( "$key=$value&amp;" );
+ }
+ }
+ $url = substr( $url, 0, -5 );
+
+ if ( is_ssl() ) {
+ $url = str_replace( 'http://', 'https://', $url );
+ }
+
+ $css_class = 'googlemaps';
+
+ if ( ! empty( $atts['align'] ) && in_array( strtolower( $atts['align'] ), array( 'left', 'center', 'right' ), true ) ) {
+ $atts['align'] = strtolower( $atts['align'] );
+
+ if ( 'left' === $atts['align'] ) {
+ $css_class .= ' alignleft';
+ } elseif ( 'center' === $atts['align'] ) {
+ $css_class .= ' aligncenter';
+ } elseif ( 'right' === $atts['align'] ) {
+ $css_class .= ' alignright';
+ }
+ }
+
+ return '<div class="' . esc_attr( $css_class ) . '"><iframe width="' . $width . '" height="' . $height . '" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="' . $url . '"></iframe></div>';
+ }
+}
+add_shortcode( 'googlemaps', 'jetpack_googlemaps_shortcode' );
diff --git a/plugins/jetpack/modules/shortcodes/googleplus.php b/plugins/jetpack/modules/shortcodes/googleplus.php
new file mode 100644
index 00000000..21211ee4
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/googleplus.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Google+ embeds
+ * Google+ has shut down. Output the link for history's sake.
+ * Other than that, there's not much we can do.
+ *
+ * @package Jetpack
+ */
+
+define( 'JETPACK_GOOGLEPLUS_EMBED_REGEX', '#^https?://plus\.(sandbox\.)?google\.com/(u/\d+/)?([^/]+)/posts/([^/]+)$#' );
+
+/*
+ * Example URL: https://plus.google.com/114986219448604314131/posts/LgHkesWCmJo
+ * Alternate example: https://plus.google.com/u/0/100004581596612508203/posts/2UKwN67MBQs (note the /u/0/)
+ */
+wp_embed_register_handler( 'googleplus', JETPACK_GOOGLEPLUS_EMBED_REGEX, 'jetpack_deprecated_embed_handler' );
+
+add_shortcode( 'googleplus', 'jetpack_googleplus_shortcode_handler' );
+
+/**
+ * Display the Google+ shortcode.
+ *
+ * @param array $atts Shortcode attributes.
+ */
+function jetpack_googleplus_shortcode_handler( $atts ) {
+ global $wp_embed;
+
+ if ( empty( $atts['url'] ) ) {
+ return;
+ }
+
+ if ( ! preg_match( JETPACK_GOOGLEPLUS_EMBED_REGEX, $atts['url'] ) ) {
+ return;
+ }
+
+ return sprintf( '<p>%s</p>', $wp_embed->shortcode( $atts, $atts['url'] ) );
+}
diff --git a/plugins/jetpack/modules/shortcodes/gravatar.php b/plugins/jetpack/modules/shortcodes/gravatar.php
new file mode 100644
index 00000000..cb709af9
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/gravatar.php
@@ -0,0 +1,163 @@
+<?php
+/**
+ * Gravatar shortcode for avatar and profile.
+ *
+ * Usage:
+ *
+ * [gravatar email="user@example.org" size="48"]
+ * [gravatar_profile who="user@example.org"]
+ *
+ * @package Jetpack
+ */
+
+add_shortcode( 'gravatar', 'jetpack_gravatar_shortcode' );
+add_shortcode( 'gravatar_profile', 'jetpack_gravatar_profile_shortcode' );
+
+/**
+ * Get gravatar using the email provided at the specified size.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode attributes.
+ *
+ * @return bool|string
+ */
+function jetpack_gravatar_shortcode( $atts ) {
+ $atts = shortcode_atts(
+ array(
+ 'email' => '',
+ 'size' => 96,
+ ),
+ $atts
+ );
+
+ if ( empty( $atts['email'] ) || ! is_email( $atts['email'] ) ) {
+ return false;
+ }
+
+ $atts['size'] = intval( $atts['size'] );
+ if ( 0 > $atts['size'] ) {
+ $atts['size'] = 96;
+ }
+
+ return get_avatar( $atts['email'], $atts['size'] );
+}
+
+/**
+ * Display Gravatar profile
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode attributes.
+ *
+ * @uses shortcode_atts()
+ * @uses get_user_by()
+ * @uses is_email()
+ * @uses sanitize_email()
+ * @uses sanitize_user()
+ * @uses set_url_scheme()
+ * @uses wpcom_get_avatar_url()
+ * @uses get_user_attribute()
+ * @uses esc_url()
+ * @uses esc_html()
+ * @uses _e()
+ *
+ * @return string
+ */
+function jetpack_gravatar_profile_shortcode( $atts ) {
+ // Give each use of the shortcode a unique ID.
+ static $instance = 0;
+
+ // Process passed attributes.
+ $atts = shortcode_atts(
+ array(
+ 'who' => null,
+ ),
+ $atts,
+ 'jetpack_gravatar_profile'
+ );
+
+ // Can specify username, user ID, or email address.
+ if ( is_numeric( $atts['who'] ) ) {
+ $user = get_user_by( 'id', (int) $atts['who'] );
+ } elseif ( is_email( $atts['who'] ) ) {
+ $user = get_user_by( 'email', sanitize_email( $atts['who'] ) );
+ } elseif ( is_string( $atts['who'] ) ) {
+ $user = get_user_by( 'login', sanitize_user( $atts['who'] ) );
+ } else {
+ $user = false;
+ }
+
+ // Bail if we don't have a user.
+ if ( false === $user ) {
+ return false;
+ }
+
+ // Render the shortcode.
+ $gravatar_url = set_url_scheme( 'http://gravatar.com/' . $user->user_login );
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $avatar_url = wpcom_get_avatar_url( $user->ID, 96 );
+ $avatar_url = $avatar_url[0];
+ $user_location = get_user_attribute( $user->ID, 'location' );
+ } else {
+ $avatar_url = get_avatar_url( $user->user_email, array( 'size' => 96 ) );
+ $user_location = get_user_meta( $user->ID, 'location', true );
+ }
+
+ ob_start();
+
+ ?>
+ <script type="text/javascript">
+ ( function() {
+ if ( null === document.getElementById( 'gravatar-profile-embed-styles' ) ) {
+ var headID = document.getElementsByTagName( 'head' )[0];
+ var styleNode = document.createElement( 'style' );
+ styleNode.type = 'text/css';
+ styleNode.id = 'gravatar-profile-embed-styles';
+
+ var gCSS = '.grofile-wrap { border: solid 1px #eee; padding: 10px; } .grofile { padding: 0 0 5px 0; } .grofile-left { float: left; display: block; width: 96px; margin-right: 15px; } .grofile .gravatar { margin-bottom: 5px; } .grofile-clear { clear: left; font-size: 1px; height: 1px; } .grofile ul li a { text-indent: -99999px; } .grofile .grofile-left a:hover { text-decoration: none !important; border: none !important; } .grofile-name { margin-top: 0; }';
+
+ if ( document.all ) {
+ styleNode.innerText = gCSS;
+ } else {
+ styleNode.textContent = gCSS;
+ }
+
+ headID.appendChild( styleNode );
+ }
+ } )();
+ </script>
+
+ <div class="grofile vcard" id="grofile-embed-<?php echo esc_attr( $instance ); ?>">
+ <div class="grofile-inner">
+ <div class="grofile-left">
+ <div class="grofile-img">
+ <a href="<?php echo esc_url( $gravatar_url ); ?>">
+ <img src="<?php echo esc_url( $avatar_url ); ?>" width="96" height="96" class="no-grav gravatar photo" />
+ </a>
+ </div>
+ </div>
+ <div class="grofile-right">
+ <p class="grofile-name fn">
+ <strong><?php echo esc_html( $user->display_name ); ?></strong>
+ <?php
+ if ( ! empty( $user_location ) ) :
+ ?>
+ <br><span class="grofile-location adr"><?php echo esc_html( $user_location ); ?></span><?php endif; ?>
+ </p>
+ <p class="grofile-bio"><strong><?php esc_html_e( 'Bio:', 'jetpack' ); ?></strong> <?php echo wp_kses_post( $user->description ); ?></p>
+ <p class="grofile-view">
+ <a href="<?php echo esc_url( $gravatar_url ); ?>"><?php esc_html_e( 'View complete profile', 'jetpack' ); ?></a>
+ </p>
+ </div>
+ <span class="grofile-clear">&nbsp;</span>
+ </div>
+ </div>
+ <?php
+
+ // Increment and return the rendered profile.
+ $instance++;
+
+ return ob_get_clean();
+}
diff --git a/plugins/jetpack/modules/shortcodes/houzz.php b/plugins/jetpack/modules/shortcodes/houzz.php
new file mode 100644
index 00000000..01a18869
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/houzz.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Houzz Embed
+ *
+ * Examples:
+ * Post content:
+ * - [houzz=http://www.houzz.com/pro/james-crisp]
+ * - http://www.houzz.com/pro/james-crisp
+ * Blog sidebar: [houzz=http://www.houzz.com/profile/alon w=200 h=300]
+ *
+ * @package Jetpack
+ */
+
+// Register oEmbed provider.
+wp_oembed_add_provider( '#https?://(.+?\.)?houzz\.(com|co\.uk|com\.au|de|fr|ru|jp|it|es|dk|se)/.*#i', 'https://www.houzz.com/oembed', true );
+
+/**
+ * Display shortcode
+ *
+ * @param array $atts Shortcode attributes.
+ */
+function jetpack_houzz_shortcode( $atts ) {
+ $url = substr( $atts[0], 1 );
+ $args = array();
+ if ( isset( $atts['w'] ) && is_numeric( $atts['w'] ) ) {
+ $args['width'] = $atts['w'];
+ }
+ if ( isset( $atts['h'] ) && is_numeric( $atts['h'] ) ) {
+ $args['height'] = $atts['h'];
+ }
+ $oembed = _wp_oembed_get_object();
+ return $oembed->get_html( $url, $args );
+}
+add_shortcode( 'houzz', 'jetpack_houzz_shortcode' );
diff --git a/plugins/jetpack/modules/shortcodes/hulu.php b/plugins/jetpack/modules/shortcodes/hulu.php
new file mode 100644
index 00000000..38203d4c
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/hulu.php
@@ -0,0 +1,275 @@
+<?php
+/**
+ * Hulu Shortcode
+ *
+ * [hulu 369061]
+ * [hulu id=369061]
+ * [hulu id=369061 width=512 height=288 start_time="10" end_time="20" thumbnail_frame="10"]
+ * [hulu http://www.hulu.com/watch/369061]
+ * [hulu id=gQ6Z0I990IWv_VFQI2J7Eg width=512 height=288]
+ *
+ * <object width="512" height="288">
+ * <param name="movie" value="http://www.hulu.com/embed/gQ6Z0I990IWv_VFQI2J7Eg"></param>
+ * <param name="allowFullScreen" value="true"></param>
+ * <embed src="http://www.hulu.com/embed/gQ6Z0I990IWv_VFQI2J7Eg" type="application/x-shockwave-flash" width="512" height="288" allowFullScreen="true"></embed>
+ * </object>
+ *
+ * @package Jetpack
+ */
+
+if ( get_option( 'embed_autourls' ) ) {
+
+ // Convert hulu URLS to shortcodes for old comments, saved before comments for shortcodes were enabled.
+ add_filter( 'comment_text', 'jetpack_hulu_link', 1 );
+}
+
+add_shortcode( 'hulu', 'jetpack_hulu_shortcode' );
+
+/**
+ * Return a Hulu video ID from a given set to attributes.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode parameters.
+ *
+ * @return string $id Hulu video ID.
+ */
+function jetpack_shortcode_get_hulu_id( $atts ) {
+ // This will catch an id explicitly defined as such, or assume any param without a label is the id. First found is used.
+ if ( isset( $atts['id'] ) ) {
+ // First we check to see if [hulu id=369061] or [hulu id=gQ6Z0I990IWv_VFQI2J7Eg] was used.
+ $id = esc_attr( $atts['id'] );
+ } elseif ( isset( $atts[0] ) && preg_match( '|www\.hulu\.com/watch/(\d+)|i', $atts[0], $match ) ) {
+ // this checks for [hulu http://www.hulu.com/watch/369061].
+ $id = (int) $match[1];
+ } elseif ( isset( $atts[0] ) ) {
+ // This checks for [hulu 369061] or [hulu 65yppv6xqa45s5n7_m1wng].
+ $id = esc_attr( $atts[0] );
+ } else {
+ $id = 0;
+ }
+
+ return $id;
+}
+
+/**
+ * Convert a Hulu shortcode into an embed code.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts An array of shortcode attributes.
+ *
+ * @return string The embed code for the Hulu video.
+ */
+function jetpack_hulu_shortcode( $atts ) {
+ global $content_width;
+
+ // Set a default content width, if it's not specified.
+ $attr = shortcode_atts(
+ array(
+ 'id' => '',
+ 'width' => $content_width ? $content_width : 640,
+ 'start_time' => '',
+ 'end_time' => '',
+ 'thumbnail_frame' => '',
+ ),
+ $atts
+ );
+
+ $id = jetpack_shortcode_get_hulu_id( $atts );
+ if ( ! $id ) {
+ return '<!-- Hulu Error: Hulu shortcode syntax invalid. -->';
+ }
+
+ $start_time = 0;
+ if ( is_numeric( $attr['start_time'] ) ) {
+ $start_time = intval( $attr['start_time'] );
+ }
+ if ( is_numeric( $attr['end_time'] ) && intval( $attr['end_time'] ) > $start_time ) {
+ $end_time = intval( $attr['end_time'] );
+ }
+ if ( is_numeric( $attr['thumbnail_frame'] ) ) {
+ $thumbnail_frame = intval( $attr['thumbnail_frame'] );
+ }
+
+ // check to see if $id is 76560 else we assume it's gQ6Z0I990IWv_VFQI2J7Eg
+ // If id is numeric, we'll send it off to the hulu oembed api to get the embed URL (and non-numeric id).
+ if ( is_numeric( $id ) ) {
+ $transient_key = "hulu-$id";
+ $transient_value = get_transient( $transient_key );
+
+ if ( false === $transient_value ) {
+ // let's make a cross-site http request out to the hulu oembed api.
+ $oembed_url = sprintf(
+ 'https://www.hulu.com/api/oembed.json?url=%s',
+ rawurlencode( 'https://www.hulu.com/watch/' . esc_attr( $id ) )
+ );
+ $response = wp_remote_get( $oembed_url );
+ $response_code = wp_remote_retrieve_response_code( $response );
+ $response_message = wp_remote_retrieve_response_message( $response );
+ if ( 200 !== $response_code && ! empty( $response_message ) ) {
+ return "<!-- Hulu Error: Hulu shortcode http error $response_message -->";
+ } elseif ( 200 !== $response_code ) {
+ return "<!-- Hulu Error: Hulu shortcode unknown error occurred, $response_code -->";
+ } else {
+ $response_body = wp_remote_retrieve_body( $response );
+ $json = json_decode( $response_body );
+
+ // Pull out id from embed url (from oembed API).
+ $embed_url_params = array();
+ parse_str( wp_parse_url( $json->embed_url, PHP_URL_QUERY ), $embed_url_params );
+
+ if ( isset( $embed_url_params['eid'] ) ) {
+ $id = $embed_url_params['eid'];
+ }
+ // let's cache this response indefinitely.
+ set_transient( $transient_key, $id );
+ }
+ } else {
+ $id = $transient_value;
+ }
+ }
+
+ if ( ! $id ) {
+ return '<!-- Hulu Error: Not a Hulu video. -->';
+ }
+
+ $query_args = array();
+ $query_args['eid'] = esc_attr( $id );
+ if ( isset( $start_time ) ) {
+ $query_args['st'] = intval( $start_time );
+ }
+ if ( isset( $end_time ) ) {
+ $query_args['et'] = intval( $end_time );
+ }
+ if ( isset( $thumbnail_frame ) ) {
+ $query_args['it'] = 'i' . intval( $thumbnail_frame );
+ }
+
+ $iframe_url = add_query_arg( $query_args, 'https://www.hulu.com/embed.html' );
+ $width = intval( $attr['width'] );
+ $height = round( ( $width / 640 ) * 360 );
+
+ $html = sprintf(
+ '<div class="embed-hulu" style="text-align: center;"><iframe src="%s" width="%s" height="%s" style="border:0;" scrolling="no" webkitAllowFullScreen
+mozallowfullscreen allowfullscreen></iframe></div>',
+ esc_url( $iframe_url ),
+ esc_attr( $width ),
+ esc_attr( $height )
+ );
+ $html = apply_filters( 'video_embed_html', $html );
+
+ return $html;
+}
+
+/**
+ * Callback to convert Hulu links in comments into a embed src.
+ *
+ * @since 4.5.0
+ *
+ * @param array $matches Array of matches from regex.
+ *
+ * @return string
+ */
+function jetpack_hulu_link_callback( $matches ) {
+ $video_id = $matches[4];
+
+ // Make up an embed src to pass to the shortcode reversal function.
+ $attrs = array(
+ 'src' => 'https://www.hulu.com/embed.html?eid=' . esc_attr( $video_id ),
+ );
+
+ return wpcom_shortcodereverse_huluhelper( $attrs );
+}
+
+/**
+ * Convert Hulu links in comments into a Hulu shortcode.
+ *
+ * @since 4.5.0
+ *
+ * @param string $content Post content.
+ *
+ * @return string
+ */
+function jetpack_hulu_link( $content ) {
+ $content = preg_replace_callback( '!^(http(s)?://)?(www\.)?hulu\.com\/watch\/([0-9]+)$!im', 'jetpack_hulu_link_callback', $content );
+
+ return $content;
+}
+
+/**
+ * Makes a Hulu shortcode from $attrs and $pattern
+ *
+ * @since 4.5.0
+ *
+ * @param array $attrs Shortcode attributes.
+ *
+ * @return string
+ */
+function wpcom_shortcodereverse_huluhelper( $attrs ) {
+ $attrs = wpcom_shortcodereverse_parseattr( $attrs );
+
+ $src_attributes = array();
+ parse_str( wp_parse_url( $attrs['src'], PHP_URL_QUERY ), $src_attributes );
+
+ $attrs = array_merge( $attrs, $src_attributes );
+
+ // If we don't have an eid, we can't do anything. Just send back the src string.
+ if ( ! isset( $attrs['eid'] ) ) {
+ return $attrs['src'];
+ }
+
+ $shortcode = '[hulu id=' . esc_attr( $attrs['eid'] );
+
+ if ( $attrs['width'] ) {
+ $shortcode .= ' width=' . intval( $attrs['width'] );
+ }
+
+ if ( $attrs['height'] ) {
+ $shortcode .= ' height=' . intval( $attrs['height'] );
+ }
+
+ if ( $attrs['st'] ) {
+ $shortcode .= ' start_time=' . intval( $attrs['st'] );
+ }
+
+ if ( $attrs['et'] ) {
+ $shortcode .= ' end_time=' . intval( $attrs['et'] );
+ }
+
+ if ( $attrs['it'] ) {
+ // the thumbnail frame attribute comes with an i in front of the value, so we've got to remove that.
+ $shortcode .= ' thumbnail_frame=' . intval( ltrim( $attrs['it'], 'i' ) );
+ }
+ $shortcode .= ']';
+
+ return $shortcode;
+}
+
+/**
+ * Initiates process to convert iframe HTML into a Hulu shortcode.
+ *
+ * Example:
+ * <iframe width="512" height="288" src="http://www.hulu.com/embed.html?eid=nlg_ios3tutcfrhatkiaow&et=20&st=10&it=i11" frameborder="0" scrolling="no" webkitAllowFullScreen mozallowfullscreen allowfullscreen></iframe>
+ *
+ * Converts to:
+ * [hulu id=nlg_ios3tutcfrhatkiaow width=512 height=288 start_time=10 end_time=20 thumbnail_frame=11]
+ *
+ * @since 4.5.0
+ *
+ * @param array $attrs Shortcode attributes.
+ *
+ * @return string
+ */
+function wpcom_shortcodereverse_huluembed( $attrs ) {
+
+ $shortcode = wpcom_shortcodereverse_huluhelper( $attrs );
+
+ if ( '[' === substr( $shortcode, 0, 1 ) ) {
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extras', 'html_to_shortcode', 'hulu-embed' );
+ }
+
+ return $shortcode;
+}
+Filter_Embedded_HTML_Objects::register( '#^https?://www.hulu.com/embed.html#i', 'wpcom_shortcodereverse_huluembed', true );
diff --git a/plugins/jetpack/modules/shortcodes/images/collapse.png b/plugins/jetpack/modules/shortcodes/images/collapse.png
new file mode 100644
index 00000000..921057f1
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/images/collapse.png
Binary files differ
diff --git a/plugins/jetpack/modules/shortcodes/images/expand.png b/plugins/jetpack/modules/shortcodes/images/expand.png
new file mode 100644
index 00000000..b1e0c56f
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/images/expand.png
Binary files differ
diff --git a/plugins/jetpack/modules/shortcodes/images/slide-nav.png b/plugins/jetpack/modules/shortcodes/images/slide-nav.png
new file mode 100644
index 00000000..7fa82c4f
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/images/slide-nav.png
Binary files differ
diff --git a/plugins/jetpack/modules/shortcodes/img/slideshow-controls-2x.png b/plugins/jetpack/modules/shortcodes/img/slideshow-controls-2x.png
new file mode 100644
index 00000000..2c76ac05
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/img/slideshow-controls-2x.png
Binary files differ
diff --git a/plugins/jetpack/modules/shortcodes/img/slideshow-controls.png b/plugins/jetpack/modules/shortcodes/img/slideshow-controls.png
new file mode 100644
index 00000000..09ca4871
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/img/slideshow-controls.png
Binary files differ
diff --git a/plugins/jetpack/modules/shortcodes/img/slideshow-loader.gif b/plugins/jetpack/modules/shortcodes/img/slideshow-loader.gif
new file mode 100644
index 00000000..ce1c594e
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/img/slideshow-loader.gif
Binary files differ
diff --git a/plugins/jetpack/modules/shortcodes/instagram.php b/plugins/jetpack/modules/shortcodes/instagram.php
new file mode 100644
index 00000000..85c675b4
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/instagram.php
@@ -0,0 +1,265 @@
+<?php
+/**
+ * Instagram Embeds.
+ *
+ * Full links: https://www.instagram.com/p/BnMOk_FFsxg/
+ * https://www.instagram.com/tv/BkQjCfsBIzi/
+ * [instagram url=https://www.instagram.com/p/BnMOk_FFsxg/]
+ * [instagram url=https://www.instagram.com/p/BZoonmAHvHf/ width=320]
+ * Embeds can be converted to a shortcode when the author does not have unfiltered_html caps:
+ * <blockquote class="instagram-media" data-instgrm-captioned data-instgrm-version="2" style=" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:658px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"><div style="padding:8px;"><div style=" background:#F8F8F8; line-height:0; margin-top:40px; padding-bottom:55%; padding-top:45%; text-align:center; width:100%;"><div style="position:relative;"><div style=" -webkit-animation:dkaXkpbBxI 1s ease-out infinite; animation:dkaXkpbBxI 1s ease-out infinite; background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAAAGFBMVEUiIiI9PT0eHh4gIB4hIBkcHBwcHBwcHBydr+JQAAAACHRSTlMABA4YHyQsM5jtaMwAAADfSURBVDjL7ZVBEgMhCAQBAf//42xcNbpAqakcM0ftUmFAAIBE81IqBJdS3lS6zs3bIpB9WED3YYXFPmHRfT8sgyrCP1x8uEUxLMzNWElFOYCV6mHWWwMzdPEKHlhLw7NWJqkHc4uIZphavDzA2JPzUDsBZziNae2S6owH8xPmX8G7zzgKEOPUoYHvGz1TBCxMkd3kwNVbU0gKHkx+iZILf77IofhrY1nYFnB/lQPb79drWOyJVa/DAvg9B/rLB4cC+Nqgdz/TvBbBnr6GBReqn/nRmDgaQEej7WhonozjF+Y2I/fZou/qAAAAAElFTkSuQmCC); display:block; height:44px; margin:0 auto -44px; position:relative; top:-44px; width:44px;"></div><span style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:12px; font-style:normal; font-weight:bold; position:relative; top:15px;">Loading</span></div></div><p style=" font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin:8px 0 0 0; padding:0 4px; word-wrap:break-word;"> Balloons</p><p style=" line-height:32px; margin-bottom:0; margin-top:8px; padding:0; text-align:center;"> <a href="https://instagram.com/p/r9vfPrmjeB/" style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; text-decoration:none;" target="_top"> View on Instagram</a></p></div><style>@-webkit-keyframes"dkaXkpbBxI"{ 0%{opacity:0.5;} 50%{opacity:1;} 100%{opacity:0.5;} } @keyframes"dkaXkpbBxI"{ 0%{opacity:0.5;} 50%{opacity:1;} 100%{opacity:0.5;} }</style></blockquote>
+ * <script async defer src="https://platform.instagram.com/en_US/embeds.js"></script>
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Embed Reversal for Instagram
+ *
+ * Hooked to pre_kses, converts an embed code from Instagram.com to an oEmbeddable URL.
+ *
+ * @param string $content Post content.
+ *
+ * @return string The filtered or the original content.
+ **/
+function jetpack_instagram_embed_reversal( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, 'instagram.com' ) ) {
+ return $content;
+ }
+
+ /*
+ * Sample embed code:
+ * <blockquote class="instagram-media" data-instgrm-captioned data-instgrm-version="2" style=" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:658px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"><div style="padding:8px;"><div style=" background:#F8F8F8; line-height:0; margin-top:40px; padding-bottom:55%; padding-top:45%; text-align:center; width:100%;"><div style="position:relative;"><div style=" -webkit-animation:dkaXkpbBxI 1s ease-out infinite; animation:dkaXkpbBxI 1s ease-out infinite; background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAAAGFBMVEUiIiI9PT0eHh4gIB4hIBkcHBwcHBwcHBydr+JQAAAACHRSTlMABA4YHyQsM5jtaMwAAADfSURBVDjL7ZVBEgMhCAQBAf//42xcNbpAqakcM0ftUmFAAIBE81IqBJdS3lS6zs3bIpB9WED3YYXFPmHRfT8sgyrCP1x8uEUxLMzNWElFOYCV6mHWWwMzdPEKHlhLw7NWJqkHc4uIZphavDzA2JPzUDsBZziNae2S6owH8xPmX8G7zzgKEOPUoYHvGz1TBCxMkd3kwNVbU0gKHkx+iZILf77IofhrY1nYFnB/lQPb79drWOyJVa/DAvg9B/rLB4cC+Nqgdz/TvBbBnr6GBReqn/nRmDgaQEej7WhonozjF+Y2I/fZou/qAAAAAElFTkSuQmCC); display:block; height:44px; margin:0 auto -44px; position:relative; top:-44px; width:44px;"></div><span style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:12px; font-style:normal; font-weight:bold; position:relative; top:15px;">Loading</span></div></div><p style=" font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin:8px 0 0 0; padding:0 4px; word-wrap:break-word;"> Balloons</p><p style=" line-height:32px; margin-bottom:0; margin-top:8px; padding:0; text-align:center;"> <a href="https://instagram.com/p/r9vfPrmjeB/" style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; text-decoration:none;" target="_top"> View on Instagram</a></p></div><style>@-webkit-keyframes"dkaXkpbBxI"{ 0%{opacity:0.5;} 50%{opacity:1;} 100%{opacity:0.5;} } @keyframes"dkaXkpbBxI"{ 0%{opacity:0.5;} 50%{opacity:1;} 100%{opacity:0.5;} }</style></blockquote>
+ * <script async defer src="https://platform.instagram.com/en_US/embeds.js"></script>
+ */
+
+ $regexes = array();
+
+ // new style js.
+ $regexes[] = '#<blockquote[^>]+?class="instagram-media"[^>].+?>(.+?)</blockquote><script[^>]+?src="(https?:)?//platform\.instagram\.com/(.+?)/embeds\.js"></script>#ix';
+
+ // Let's play nice with the visual editor too.
+ $regexes[] = '#&lt;blockquote(?:[^&]|&(?!gt;))+?class="instagram-media"(?:[^&]|&(?!gt;)).+?&gt;(.+?)&lt;/blockquote&gt;&lt;script(?:[^&]|&(?!gt;))+?src="(https?:)?//platform\.instagram\.com/(.+?)/embeds\.js"(?:[^&]|&(?!gt;))*+&gt;&lt;/script&gt;#ix';
+
+ // old style iframe.
+ $regexes[] = '#<iframe[^>]+?src="((?:https?:)?//(?:www\.)?instagram\.com/p/([^"\'/]++)[^"\']*?)"[^>]*+>\s*?</iframe>#i';
+
+ // Let's play nice with the visual editor too.
+ $regexes[] = '#&lt;iframe(?:[^&]|&(?!gt;))+?src="((?:https?:)?//(?:www\.)instagram\.com/p/([^"\'/]++)[^"\']*?)"(?:[^&]|&(?!gt;))*+&gt;\s*?&lt;/iframe&gt;#i';
+
+ foreach ( $regexes as $regex ) {
+ if ( ! preg_match_all( $regex, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ if ( ! preg_match( '#(https?:)?//(?:www\.)?instagr(\.am|am\.com)/p/([^/]*)#i', $match[1], $url_matches ) ) {
+ continue;
+ }
+
+ // Since we support Instagram via oEmbed, we simply leave a link on a line by itself.
+ $replace_regex = sprintf( '#\s*%s\s*#', preg_quote( $match[0], '#' ) );
+ $url = esc_url( $url_matches[0] );
+
+ $content = preg_replace( $replace_regex, sprintf( "\n\n%s\n\n", $url ), $content );
+ /** This action is documented in modules/shortcodes/youtube.php */
+ do_action( 'jetpack_embed_to_shortcode', 'instagram', $url );
+ }
+ }
+
+ return $content;
+}
+
+add_filter( 'pre_kses', 'jetpack_instagram_embed_reversal' );
+
+/**
+ * Instagram's custom Embed provider.
+ * We first remove 2 different embed providers, both registered by Core.
+ * - The first is the original provider,that only supports images.
+ * - The second is tne new provider that replaced the first one in Core when Core added support for videos. https://core.trac.wordpress.org/changeset/44486
+ *
+ * Once the core embed provider is removed (one or the other, depending on your version of Core), we declare our own.
+ */
+wp_oembed_remove_provider( '#https?://(www\.)?instagr(\.am|am\.com)/p/.*#i' );
+wp_oembed_remove_provider( '#https?://(www\.)?instagr(\.am|am\.com)/(p|tv)/.*#i' );
+wp_embed_register_handler(
+ 'jetpack_instagram',
+ '#http(s?)://(www\.)?instagr(\.am|am\.com)/(p|tv)/([^\/]*)#i',
+ 'jetpack_instagram_handler'
+);
+
+/**
+ * Handle Instagram embeds (build embed from regex).
+ *
+ * @param array $matches Array of matches from the regex.
+ * @param array $atts The original unmodified attributes.
+ * @param string $url The original URL that was matched by the regex.
+ */
+function jetpack_instagram_handler( $matches, $atts, $url ) {
+ global $content_width;
+
+ // keep a copy of the passed-in URL since it's modified below.
+ $passed_url = $url;
+
+ $max_width = 698;
+ $min_width = 320;
+
+ if ( is_feed() ) {
+ // Instagram offers direct links to images, but not to videos.
+ if ( 'p' === $matches[1] ) {
+ $media_url = sprintf( 'http://instagr.am/p/%1$s/media/?size=l', $matches[2] );
+ return sprintf(
+ '<a href="%1$s" title="%2$s" target="_blank"><img src="%3$s" alt="%4$s" /></a>',
+ esc_url( $url ),
+ esc_attr__( 'View on Instagram', 'jetpack' ),
+ esc_url( $media_url ),
+ esc_html__( 'Instagram Photo', 'jetpack' )
+ );
+ } elseif ( 'tv' === $matches[1] ) {
+ return sprintf(
+ '<a href="%1$s" title="%2$s" target="_blank">%3$s</a>',
+ esc_url( $url ),
+ esc_attr__( 'View on Instagram', 'jetpack' ),
+ esc_html__( 'Instagram Video', 'jetpack' )
+ );
+ }
+ }
+
+ $atts = shortcode_atts(
+ array(
+ 'width' => isset( $content_width ) ? $content_width : $max_width,
+ 'hidecaption' => false,
+ ),
+ $atts
+ );
+
+ $atts['width'] = absint( $atts['width'] );
+ if ( $atts['width'] > $max_width ) {
+ $atts['width'] = $max_width;
+ } elseif ( $atts['width'] < $min_width ) {
+ $atts['width'] = $min_width;
+ }
+
+ // remove the modal param from the URL.
+ $url = remove_query_arg( 'modal', $url );
+
+ // force .com instead of .am for https support.
+ $url = str_replace( 'instagr.am', 'instagram.com', $url );
+
+ // The oembed endpoint expects HTTP, but HTTP requests 301 to HTTPS.
+ $instagram_http_url = str_replace( 'https://', 'http://', $url );
+
+ $url_args = array(
+ 'url' => $instagram_http_url,
+ 'maxwidth' => $atts['width'],
+ 'omitscript' => 1,
+ );
+
+ if ( $atts['hidecaption'] ) {
+ $url_args['hidecaption'] = 'true';
+ }
+
+ $url = esc_url_raw( add_query_arg( $url_args, 'https://api.instagram.com/oembed/' ) );
+
+ /**
+ * Filter Object Caching for response from Instagram.
+ *
+ * Allow enabling of object caching for the response sent by Instagram when querying for Instagram image HTML.
+ *
+ * @module shortcodes
+ *
+ * @since 3.3.0
+ *
+ * @param bool false Object caching is off by default.
+ * @param array $matches Array of Instagram URLs found in the post.
+ * @param array $atts Instagram Shortcode attributes.
+ * @param string $passed_url Instagram API URL.
+ */
+ $response_body_use_cache = apply_filters( 'instagram_cache_oembed_api_response_body', false, $matches, $atts, $passed_url );
+ $response_body = false;
+ if ( $response_body_use_cache ) {
+ $cache_key = 'oembed_response_body_' . md5( $url );
+ $response_body = wp_cache_get( $cache_key, 'instagram_embeds' );
+ }
+
+ if ( ! $response_body ) {
+ // Not using cache (default case) or cache miss.
+ $instagram_response = wp_remote_get( $url, array( 'redirection' => 0 ) );
+ if (
+ is_wp_error( $instagram_response )
+ || 200 !== $instagram_response['response']['code']
+ || empty( $instagram_response['body'] ) ) {
+ return '<!-- instagram error: invalid instagram resource -->';
+ }
+
+ $response_body = json_decode( $instagram_response['body'] );
+ if ( $response_body_use_cache ) {
+ // if caching it is short-lived since this is a "Cache-Control: no-cache" resource.
+ wp_cache_set(
+ $cache_key,
+ $response_body,
+ 'instagram_embeds',
+ HOUR_IN_SECONDS + wp_rand( 0, HOUR_IN_SECONDS )
+ );
+ }
+ }
+
+ if ( ! empty( $response_body->html ) ) {
+ wp_enqueue_script(
+ 'jetpack-instagram-embed',
+ Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/instagram.min.js', 'modules/shortcodes/js/instagram.js' ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ true
+ );
+ return $response_body->html;
+ }
+
+ return '<!-- instagram error: no embed found -->';
+}
+
+/**
+ * Handle an alternate Instagram URL format, where the username is also part of the URL.
+ * We do not actually need that username for the embed.
+ */
+wp_embed_register_handler(
+ 'jetpack_instagram_alternate_format',
+ '#https?://(?:www\.)?instagr(?:\.am|am\.com)/(?:[^/]*)/(p|tv)/([^\/]*)#i',
+ 'jetpack_instagram_alternate_format_handler'
+);
+
+/**
+ * Handle alternate Instagram embeds (build embed from regex).
+ *
+ * @param array $matches Array of matches from the regex.
+ * @param array $atts The original unmodified attributes.
+ * @param string $url The original URL that was matched by the regex.
+ */
+function jetpack_instagram_alternate_format_handler( $matches, $atts, $url ) {
+ // Replace URL saved by original Instagram URL (no username).
+ $matches[0] = esc_url_raw(
+ sprintf(
+ 'https://www.instagram.com/%1$s/%2$s',
+ $matches[1],
+ $matches[2]
+ )
+ );
+
+ return jetpack_instagram_handler( $matches, $atts, $url );
+}
+
+/**
+ * Display the Instagram shortcode.
+ *
+ * @param array $atts Shortcode attributes.
+ */
+function jetpack_shortcode_instagram( $atts ) {
+ global $wp_embed;
+
+ if ( empty( $atts['url'] ) ) {
+ return '';
+ }
+
+ return $wp_embed->shortcode( $atts, $atts['url'] );
+}
+add_shortcode( 'instagram', 'jetpack_shortcode_instagram' );
diff --git a/plugins/jetpack/modules/shortcodes/js/brightcove.js b/plugins/jetpack/modules/shortcodes/js/brightcove.js
new file mode 100644
index 00000000..0e6da190
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/js/brightcove.js
@@ -0,0 +1,29 @@
+/* global brightcove, brightcoveData */
+( function( $ ) {
+ var script = document.createElement( 'script' ),
+ tld = 'co.jp' === brightcoveData.tld ? 'co.jp' : 'com',
+ timer = false;
+
+ // Load Brightcove script
+ script.src = 'https://sadmin.brightcove.' + tld + '/js/BrightcoveExperiences.js';
+ script.type = 'text/javascript';
+ script.language = 'JavaScript';
+ document.head.appendChild( script );
+
+ // Start detection for Brightcove script loading in its object
+ try_brightcove();
+
+ // Detect if Brightcove script has loaded and bind some events once loaded
+ function try_brightcove() {
+ clearTimeout( timer );
+
+ if ( 'object' === typeof brightcove ) {
+ $( document ).ready( brightcove.createExperiences );
+ $( 'body' ).on( 'post-load', brightcove.createExperiences );
+
+ brightcove.createExperiences();
+ } else {
+ timer = setTimeout( try_brightcove, 100 );
+ }
+ }
+} )( jQuery );
diff --git a/plugins/jetpack/modules/shortcodes/js/gist.js b/plugins/jetpack/modules/shortcodes/js/gist.js
new file mode 100644
index 00000000..a97d7f91
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/js/gist.js
@@ -0,0 +1,27 @@
+( function( $, undefined ) {
+ var gistStylesheetLoaded = false,
+ gistEmbed = function() {
+ $( '.gist-oembed' ).each( function( i, el ) {
+ var url = 'https://gist.github.com/' + $( el ).data( 'gist' );
+
+ $.ajax( {
+ url: url,
+ dataType: 'jsonp',
+ } ).done( function( response ) {
+ $( el ).replaceWith( response.div );
+
+ if ( ! gistStylesheetLoaded ) {
+ var stylesheet =
+ '<link rel="stylesheet" href="' + response.stylesheet + '" type="text/css" />';
+
+ $( 'head' ).append( stylesheet );
+
+ gistStylesheetLoaded = true;
+ }
+ } );
+ } );
+ };
+
+ $( document ).ready( gistEmbed );
+ $( 'body' ).on( 'post-load', gistEmbed );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/shortcodes/js/instagram.js b/plugins/jetpack/modules/shortcodes/js/instagram.js
new file mode 100644
index 00000000..b0d0d609
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/js/instagram.js
@@ -0,0 +1,25 @@
+/* global window */
+
+( function() {
+ var instagramEmbed = function() {
+ if (
+ 'undefined' !== typeof window.instgrm &&
+ window.instgrm.Embeds &&
+ 'function' === typeof window.instgrm.Embeds.process
+ ) {
+ window.instgrm.Embeds.process();
+ } else {
+ var s = document.createElement( 'script' );
+ s.async = true;
+ s.defer = true;
+ s.src = '//platform.instagram.com/en_US/embeds.js';
+ document.getElementsByTagName( 'body' )[ 0 ].appendChild( s );
+ }
+ };
+
+ if ( 'undefined' !== typeof jQuery && 'undefined' !== typeof infiniteScroll ) {
+ jQuery( document.body ).on( 'post-load', instagramEmbed );
+ }
+
+ instagramEmbed();
+} )();
diff --git a/plugins/jetpack/modules/shortcodes/js/jmpress.js b/plugins/jetpack/modules/shortcodes/js/jmpress.js
new file mode 100644
index 00000000..c6519cd9
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/js/jmpress.js
@@ -0,0 +1,2897 @@
+/**
+ * jmpress.js v0.4.5
+ * http://jmpressjs.github.com/jmpress.js
+ *
+ * A jQuery plugin to build a website on the infinite canvas.
+ *
+ * Copyright 2013 Kyle Robinson Young @shama & Tobias Koppers @sokra
+ * Licensed MIT
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * Based on the foundation laid by Bartek Szopka @bartaz
+ */ /*
+ * core.js
+ * The core of jmpress.js
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+
+ /**
+ * Set supported prefixes
+ *
+ * @access protected
+ * @return Function to get prefixed property
+ */
+ var pfx = ( function() {
+ var style = document.createElement( 'dummy' ).style,
+ prefixes = 'Webkit Moz O ms Khtml'.split( ' ' ),
+ memory = {};
+ return function( prop ) {
+ if ( typeof memory[ prop ] === 'undefined' ) {
+ var ucProp = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ),
+ props = ( prop + ' ' + prefixes.join( ucProp + ' ' ) + ucProp ).split( ' ' );
+ memory[ prop ] = null;
+ for ( var i in props ) {
+ if ( style[ props[ i ] ] !== undefined ) {
+ memory[ prop ] = props[ i ];
+ break;
+ }
+ }
+ }
+ return memory[ prop ];
+ };
+ } )();
+
+ /**
+ * map ex. "WebkitTransform" to "-webkit-transform"
+ */
+ function mapProperty( name ) {
+ if ( ! name ) {
+ return;
+ }
+ var index = 1 + name.substr( 1 ).search( /[A-Z]/ );
+ var prefix = name.substr( 0, index ).toLowerCase();
+ var postfix = name.substr( index ).toLowerCase();
+ return '-' + prefix + '-' + postfix;
+ }
+ function addComma( attribute ) {
+ if ( ! attribute ) {
+ return '';
+ }
+ return attribute + ',';
+ }
+ /**
+ * Return an jquery object only if it's not empty
+ */
+ function ifNotEmpty( el ) {
+ if ( el.length > 0 ) {
+ return el;
+ }
+ return null;
+ }
+
+ /**
+ * Default Settings
+ */
+ var defaults = {
+ /* CLASSES */
+ stepSelector: '.step',
+ containerClass: '',
+ canvasClass: '',
+ areaClass: '',
+ notSupportedClass: 'not-supported',
+
+ /* CONFIG */
+ fullscreen: true,
+
+ /* ANIMATION */
+ animation: {
+ transformOrigin: 'top left',
+ transitionProperty:
+ addComma( mapProperty( pfx( 'transform' ) ) ) +
+ addComma( mapProperty( pfx( 'perspective' ) ) ) +
+ 'opacity',
+ transitionDuration: '1s',
+ transitionDelay: '500ms',
+ transitionTimingFunction: 'ease-in-out',
+ transformStyle: 'preserve-3d',
+ },
+ transitionDuration: 1500,
+ };
+ var callbacks = {
+ beforeChange: 1,
+ beforeInitStep: 1,
+ initStep: 1,
+ beforeInit: 1,
+ afterInit: 1,
+ beforeDeinit: 1,
+ afterDeinit: 1,
+ applyStep: 1,
+ unapplyStep: 1,
+ setInactive: 1,
+ beforeActive: 1,
+ setActive: 1,
+ selectInitialStep: 1,
+ selectPrev: 1,
+ selectNext: 1,
+ selectHome: 1,
+ selectEnd: 1,
+ idle: 1,
+ applyTarget: 1,
+ };
+ for ( var callbackName in callbacks ) {
+ defaults[ callbackName ] = [];
+ }
+
+ /**
+ * Initialize jmpress
+ */
+ function init( args ) {
+ args = $.extend( true, {}, args || {} );
+
+ // accept functions and arrays of functions as callbacks
+ var callbackArgs = {};
+ var callbackName = null;
+ for ( callbackName in callbacks ) {
+ callbackArgs[ callbackName ] = $.isFunction( args[ callbackName ] )
+ ? [ args[ callbackName ] ]
+ : args[ callbackName ];
+ args[ callbackName ] = [];
+ }
+
+ // MERGE SETTINGS
+ var settings = $.extend( true, {}, defaults, args );
+
+ for ( callbackName in callbacks ) {
+ if ( callbackArgs[ callbackName ] ) {
+ Array.prototype.push.apply( settings[ callbackName ], callbackArgs[ callbackName ] );
+ }
+ }
+
+ /*** MEMBER VARS ***/
+
+ var jmpress = $( this ),
+ container = null,
+ area = null,
+ oldStyle = {
+ container: '',
+ area: '',
+ },
+ canvas = null,
+ current = null,
+ active = false,
+ activeSubstep = null,
+ activeDelegated = false;
+
+ /*** MEMBER FUNCTIONS ***/
+ // functions have to be called with this
+
+ /**
+ * Init a single step
+ *
+ * @param element the element of the step
+ * @param idx number of step
+ */
+ function doStepInit( element, idx ) {
+ var data = dataset( element );
+ var step = {
+ oldStyle: $( element ).attr( 'style' ) || '',
+ };
+
+ var callbackData = {
+ data: data,
+ stepData: step,
+ };
+ callCallback.call( this, 'beforeInitStep', $( element ), callbackData );
+ step.delegate = data.delegate;
+ callCallback.call( this, 'initStep', $( element ), callbackData );
+
+ $( element ).data( 'stepData', step );
+
+ if ( ! $( element ).attr( 'id' ) ) {
+ $( element ).attr( 'id', 'step-' + ( idx + 1 ) );
+ }
+
+ callCallback.call( this, 'applyStep', $( element ), callbackData );
+ }
+ /**
+ * Deinit a single step
+ *
+ * @param element the element of the step
+ */
+ function doStepDeinit( element ) {
+ var stepData = $( element ).data( 'stepData' );
+
+ $( element ).attr( 'style', stepData.oldStyle );
+
+ callCallback.call( this, 'unapplyStep', $( element ), {
+ stepData: stepData,
+ } );
+ }
+ /**
+ * Reapplies stepData to the element
+ *
+ * @param element
+ */
+ function doStepReapply( element ) {
+ callCallback.call( this, 'unapplyStep', $( element ), {
+ stepData: element.data( 'stepData' ),
+ } );
+
+ callCallback.call( this, 'applyStep', $( element ), {
+ stepData: element.data( 'stepData' ),
+ } );
+ }
+ /**
+ * Completly deinit jmpress
+ *
+ */
+ function deinit() {
+ if ( active ) {
+ callCallback.call( this, 'setInactive', active, {
+ stepData: $( active ).data( 'stepData' ),
+ reason: 'deinit',
+ } );
+ }
+ if ( current.jmpressClass ) {
+ $( jmpress ).removeClass( current.jmpressClass );
+ }
+
+ callCallback.call( this, 'beforeDeinit', $( this ), {} );
+
+ $( settings.stepSelector, jmpress ).each( function( idx ) {
+ doStepDeinit.call( jmpress, this );
+ } );
+
+ container.attr( 'style', oldStyle.container );
+ if ( settings.fullscreen ) {
+ $( 'html' ).attr( 'style', '' );
+ }
+ area.attr( 'style', oldStyle.area );
+ $( canvas )
+ .children()
+ .each( function() {
+ jmpress.append( $( this ) );
+ } );
+ if ( settings.fullscreen ) {
+ canvas.remove();
+ } else {
+ canvas.remove();
+ area.remove();
+ }
+
+ callCallback.call( this, 'afterDeinit', $( this ), {} );
+
+ $( jmpress ).data( 'jmpressmethods', false );
+ }
+ /**
+ * Call a callback
+ *
+ * @param callbackName String callback which should be called
+ * @param element some arguments to the callback
+ * @param eventData
+ */
+ function callCallback( callbackName, element, eventData ) {
+ eventData.settings = settings;
+ eventData.current = current;
+ eventData.container = container;
+ eventData.parents = element ? getStepParents( element ) : null;
+ eventData.current = current;
+ eventData.jmpress = this;
+ var result = {};
+ $.each( settings[ callbackName ], function( idx, callback ) {
+ result.value = callback.call( jmpress, element, eventData ) || result.value;
+ } );
+ return result.value;
+ }
+ /**
+ *
+ */
+ function getStepParents( el ) {
+ return $( el )
+ .parentsUntil( jmpress )
+ .not( jmpress )
+ .filter( settings.stepSelector );
+ }
+ /**
+ * Reselect the active step
+ *
+ * @param String type reason of reselecting step
+ */
+ function reselect( type ) {
+ return select( { step: active, substep: activeSubstep }, type );
+ }
+ /**
+ * Select a given step
+ *
+ * @param el element to select
+ * @param type reason of changing step
+ * @return Object element selected
+ */
+ function select( el, type ) {
+ var substep;
+ if ( $.isPlainObject( el ) ) {
+ substep = el.substep;
+ el = el.step;
+ }
+ if ( typeof el === 'string' ) {
+ el = jmpress.find( el ).first();
+ }
+ if ( ! el || ! $( el ).data( 'stepData' ) ) {
+ return false;
+ }
+
+ scrollFix.call( this );
+
+ var step = $( el ).data( 'stepData' );
+
+ var cancelSelect = false;
+ callCallback.call( this, 'beforeChange', el, {
+ stepData: step,
+ reason: type,
+ cancel: function() {
+ cancelSelect = true;
+ },
+ } );
+ if ( cancelSelect ) {
+ return undefined;
+ }
+
+ var target = {};
+
+ var delegated = el;
+ if ( $( el ).data( 'stepData' ).delegate ) {
+ delegated =
+ ifNotEmpty(
+ $( el )
+ .parentsUntil( jmpress )
+ .filter( settings.stepSelector )
+ .filter( step.delegate )
+ ) ||
+ ifNotEmpty( $( el ).near( step.delegate ) ) ||
+ ifNotEmpty( $( el ).near( step.delegate, true ) ) ||
+ ifNotEmpty( $( step.delegate, jmpress ) );
+ if ( delegated ) {
+ step = delegated.data( 'stepData' );
+ } else {
+ // Do not delegate if expression not found
+ delegated = el;
+ }
+ }
+ if ( activeDelegated ) {
+ callCallback.call( this, 'setInactive', activeDelegated, {
+ stepData: $( activeDelegated ).data( 'stepData' ),
+ delegatedFrom: active,
+ reason: type,
+ target: target,
+ nextStep: delegated,
+ nextSubstep: substep,
+ nextStepData: step,
+ } );
+ }
+ var callbackData = {
+ stepData: step,
+ delegatedFrom: el,
+ reason: type,
+ target: target,
+ substep: substep,
+ prevStep: activeDelegated,
+ prevSubstep: activeSubstep,
+ prevStepData: activeDelegated && $( activeDelegated ).data( 'stepData' ),
+ };
+ callCallback.call( this, 'beforeActive', delegated, callbackData );
+ callCallback.call( this, 'setActive', delegated, callbackData );
+
+ // Set on step class on root element
+ if ( current.jmpressClass ) {
+ $( jmpress ).removeClass( current.jmpressClass );
+ }
+ $( jmpress ).addClass( ( current.jmpressClass = 'step-' + $( delegated ).attr( 'id' ) ) );
+ if ( current.jmpressDelegatedClass ) {
+ $( jmpress ).removeClass( current.jmpressDelegatedClass );
+ }
+ $( jmpress ).addClass(
+ ( current.jmpressDelegatedClass = 'delegating-step-' + $( el ).attr( 'id' ) )
+ );
+
+ callCallback.call(
+ this,
+ 'applyTarget',
+ delegated,
+ $.extend(
+ {
+ canvas: canvas,
+ area: area,
+ beforeActive: activeDelegated,
+ },
+ callbackData
+ )
+ );
+
+ active = el;
+ activeSubstep = callbackData.substep;
+ activeDelegated = delegated;
+
+ if ( current.idleTimeout ) {
+ clearTimeout( current.idleTimeout );
+ }
+ current.idleTimeout = setTimeout( function() {
+ callCallback.call( this, 'idle', delegated, callbackData );
+ }, Math.max( 1, settings.transitionDuration - 100 ) );
+
+ return delegated;
+ }
+ /**
+ * This should fix ANY kind of buggy scrolling
+ */
+ function scrollFix() {
+ ( function fix() {
+ if ( $( container )[ 0 ].tagName === 'BODY' ) {
+ try {
+ window.scrollTo( 0, 0 );
+ } catch ( e ) {}
+ }
+ $( container ).scrollTop( 0 );
+ $( container ).scrollLeft( 0 );
+ function check() {
+ if ( $( container ).scrollTop() !== 0 || $( container ).scrollLeft() !== 0 ) {
+ fix();
+ }
+ }
+ setTimeout( check, 1 );
+ setTimeout( check, 10 );
+ setTimeout( check, 100 );
+ setTimeout( check, 200 );
+ setTimeout( check, 400 );
+ } )();
+ }
+ /**
+ * Alias for select
+ */
+ function goTo( el ) {
+ return select.call( this, el, 'jump' );
+ }
+ /**
+ * Goto Next Slide
+ *
+ * @return Object newly active slide
+ */
+ function next() {
+ return select.call(
+ this,
+ callCallback.call( this, 'selectNext', active, {
+ stepData: $( active ).data( 'stepData' ),
+ substep: activeSubstep,
+ } ),
+ 'next'
+ );
+ }
+ /**
+ * Goto Previous Slide
+ *
+ * @return Object newly active slide
+ */
+ function prev() {
+ return select.call(
+ this,
+ callCallback.call( this, 'selectPrev', active, {
+ stepData: $( active ).data( 'stepData' ),
+ substep: activeSubstep,
+ } ),
+ 'prev'
+ );
+ }
+ /**
+ * Goto First Slide
+ *
+ * @return Object newly active slide
+ */
+ function home() {
+ return select.call(
+ this,
+ callCallback.call( this, 'selectHome', active, {
+ stepData: $( active ).data( 'stepData' ),
+ } ),
+ 'home'
+ );
+ }
+ /**
+ * Goto Last Slide
+ *
+ * @return Object newly active slide
+ */
+ function end() {
+ return select.call(
+ this,
+ callCallback.call( this, 'selectEnd', active, {
+ stepData: $( active ).data( 'stepData' ),
+ } ),
+ 'end'
+ );
+ }
+ /**
+ * Manipulate the canvas
+ *
+ * @param props
+ * @return Object
+ */
+ function canvasMod( props ) {
+ css( canvas, props || {} );
+ return $( canvas );
+ }
+ /**
+ * Return current step
+ *
+ * @return Object
+ */
+ function getActive() {
+ return activeDelegated && $( activeDelegated );
+ }
+ /**
+ * fire a callback
+ *
+ * @param callbackName
+ * @param element
+ * @param eventData
+ * @return void
+ */
+ function fire( callbackName, element, eventData ) {
+ if ( ! callbacks[ callbackName ] ) {
+ $.error( 'callback ' + callbackName + ' is not registered.' );
+ } else {
+ return callCallback.call( this, callbackName, element, eventData );
+ }
+ }
+
+ /**
+ * PUBLIC METHODS LIST
+ */
+ jmpress.data( 'jmpressmethods', {
+ select: select,
+ reselect: reselect,
+ scrollFix: scrollFix,
+ goTo: goTo,
+ next: next,
+ prev: prev,
+ home: home,
+ end: end,
+ canvas: canvasMod,
+ container: function() {
+ return container;
+ },
+ settings: function() {
+ return settings;
+ },
+ active: getActive,
+ current: function() {
+ return current;
+ },
+ fire: fire,
+ init: function( step ) {
+ doStepInit.call( this, $( step ), current.nextIdNumber++ );
+ },
+ deinit: function( step ) {
+ if ( step ) {
+ doStepDeinit.call( this, $( step ) );
+ } else {
+ deinit.call( this );
+ }
+ },
+ reapply: doStepReapply,
+ } );
+
+ /**
+ * Check for support
+ * This will be removed in near future, when support is coming
+ *
+ * @access protected
+ * @return void
+ */
+ function checkSupport() {
+ var ua = navigator.userAgent.toLowerCase();
+ return ua.search( /(iphone)|(ipod)|(android)/ ) === -1 || ua.search( /(chrome)/ ) !== -1;
+ }
+
+ // BEGIN INIT
+
+ // CHECK FOR SUPPORT
+ if ( checkSupport() === false ) {
+ if ( settings.notSupportedClass ) {
+ jmpress.addClass( settings.notSupportedClass );
+ }
+ return;
+ } else {
+ if ( settings.notSupportedClass ) {
+ jmpress.removeClass( settings.notSupportedClass );
+ }
+ }
+
+ // grabbing all steps
+ var steps = $( settings.stepSelector, jmpress );
+
+ // GERNERAL INIT OF FRAME
+ container = jmpress;
+ area = $( '<div />' );
+ canvas = $( '<div />' );
+ $( jmpress )
+ .children()
+ .filter( steps )
+ .each( function() {
+ canvas.append( $( this ) );
+ } );
+ if ( settings.fullscreen ) {
+ container = $( 'body' );
+ $( 'html' ).css( {
+ overflow: 'hidden',
+ } );
+ area = jmpress;
+ }
+ oldStyle.area = area.attr( 'style' ) || '';
+ oldStyle.container = container.attr( 'style' ) || '';
+ if ( settings.fullscreen ) {
+ container.css( {
+ height: '100%',
+ } );
+ jmpress.append( canvas );
+ } else {
+ container.css( {
+ position: 'relative',
+ } );
+ area.append( canvas );
+ jmpress.append( area );
+ }
+
+ $( container ).addClass( settings.containerClass );
+ $( area ).addClass( settings.areaClass );
+ $( canvas ).addClass( settings.canvasClass );
+
+ document.documentElement.style.height = '100%';
+ container.css( {
+ overflow: 'hidden',
+ } );
+
+ var props = {
+ position: 'absolute',
+ transitionDuration: '0s',
+ };
+ props = $.extend( {}, settings.animation, props );
+ css( area, props );
+ css( area, {
+ top: '50%',
+ left: '50%',
+ perspective: '1000px',
+ } );
+ css( canvas, props );
+
+ current = {};
+
+ callCallback.call( this, 'beforeInit', null, {} );
+
+ // INITIALIZE EACH STEP
+ steps.each( function( idx ) {
+ doStepInit.call( jmpress, this, idx );
+ } );
+ current.nextIdNumber = steps.length;
+
+ callCallback.call( this, 'afterInit', null, {} );
+
+ // START
+ select.call( this, callCallback.call( this, 'selectInitialStep', 'init', {} ) );
+
+ if ( settings.initClass ) {
+ $( steps ).removeClass( settings.initClass );
+ }
+ }
+ /**
+ * Return default settings
+ *
+ * @return Object
+ */
+ function getDefaults() {
+ return defaults;
+ }
+ /**
+ * Register a callback or a jmpress function
+ *
+ * @access public
+ * @param name String the name of the callback or function
+ * @param func Function? the function to be added
+ */
+ function register( name, func ) {
+ if ( $.isFunction( func ) ) {
+ if ( methods[ name ] ) {
+ $.error( 'function ' + name + ' is already registered.' );
+ } else {
+ methods[ name ] = func;
+ }
+ } else {
+ if ( callbacks[ name ] ) {
+ $.error( 'callback ' + name + ' is already registered.' );
+ } else {
+ callbacks[ name ] = 1;
+ defaults[ name ] = [];
+ }
+ }
+ }
+ /**
+ * Set CSS on element w/ prefixes
+ *
+ * @return Object element which properties were set
+ *
+ * TODO: Consider bypassing pfx and blindly set as jQuery
+ * already checks for support
+ */
+ function css( el, props ) {
+ var key,
+ pkey,
+ cssObj = {};
+ for ( key in props ) {
+ if ( props.hasOwnProperty( key ) ) {
+ pkey = pfx( key );
+ if ( pkey !== null ) {
+ cssObj[ pkey ] = props[ key ];
+ }
+ }
+ }
+ $( el ).css( cssObj );
+ return el;
+ }
+ /**
+ * Return dataset for element
+ *
+ * @param el element
+ * @return Object
+ */
+ function dataset( el ) {
+ if ( $( el )[ 0 ].dataset ) {
+ return $.extend( {}, $( el )[ 0 ].dataset );
+ }
+ function toCamelcase( str ) {
+ str = str.split( '-' );
+ for ( var i = 1; i < str.length; i++ ) {
+ str[ i ] = str[ i ].substr( 0, 1 ).toUpperCase() + str[ i ].substr( 1 );
+ }
+ return str.join( '' );
+ }
+ var returnDataset = {};
+ var attrs = $( el )[ 0 ].attributes;
+ $.each( attrs, function( idx, attr ) {
+ if ( attr.nodeName.substr( 0, 5 ) === 'data-' ) {
+ returnDataset[ toCamelcase( attr.nodeName.substr( 5 ) ) ] = attr.nodeValue;
+ }
+ } );
+ return returnDataset;
+ }
+ /**
+ * Returns true, if jmpress is initialized
+ *
+ * @return bool
+ */
+ function initialized() {
+ return !! $( this ).data( 'jmpressmethods' );
+ }
+
+ /**
+ * PUBLIC STATIC METHODS LIST
+ */
+ var methods = {
+ init: init,
+ initialized: initialized,
+ deinit: function() {},
+ css: css,
+ pfx: pfx,
+ defaults: getDefaults,
+ register: register,
+ dataset: dataset,
+ };
+
+ /**
+ * $.jmpress()
+ */
+ $.fn.jmpress = function( method ) {
+ function f() {
+ var jmpressmethods = $( this ).data( 'jmpressmethods' );
+ if ( jmpressmethods && jmpressmethods[ method ] ) {
+ return jmpressmethods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) );
+ } else if ( methods[ method ] ) {
+ return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) );
+ } else if ( callbacks[ method ] && jmpressmethods ) {
+ var settings = jmpressmethods.settings();
+ var func = Array.prototype.slice.call( arguments, 1 )[ 0 ];
+ if ( $.isFunction( func ) ) {
+ settings[ method ] = settings[ method ] || [];
+ settings[ method ].push( func );
+ }
+ } else if ( typeof method === 'object' || ! method ) {
+ return init.apply( this, arguments );
+ } else {
+ $.error( 'Method ' + method + ' does not exist on jQuery.jmpress' );
+ }
+ // to allow chaining
+ return this;
+ }
+ var args = arguments;
+ var result;
+ $( this ).each( function( idx, element ) {
+ result = f.apply( element, args );
+ } );
+ return result;
+ };
+ $.extend( {
+ jmpress: function( method ) {
+ if ( methods[ method ] ) {
+ return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) );
+ } else if ( callbacks[ method ] ) {
+ // plugin interface
+ var func = Array.prototype.slice.call( arguments, 1 )[ 0 ];
+ if ( $.isFunction( func ) ) {
+ defaults[ method ].push( func );
+ } else {
+ $.error(
+ 'Second parameter should be a function: $.jmpress( callbackName, callbackFunction )'
+ );
+ }
+ } else {
+ $.error( 'Method ' + method + ' does not exist on jQuery.jmpress' );
+ }
+ },
+ } );
+} )( jQuery, document, window );
+
+/*
+ * near.js
+ * Find steps near each other
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+
+ // add near( selector, backwards = false) to jquery
+
+ function checkAndGo( elements, func, selector, backwards ) {
+ var next;
+ elements.each( function( idx, element ) {
+ if ( backwards ) {
+ next = func( element, selector, backwards );
+ if ( next ) {
+ return false;
+ }
+ }
+ if ( $( element ).is( selector ) ) {
+ next = element;
+ return false;
+ }
+ if ( ! backwards ) {
+ next = func( element, selector, backwards );
+ if ( next ) {
+ return false;
+ }
+ }
+ } );
+ return next;
+ }
+ function findNextInChildren( item, selector, backwards ) {
+ var children = $( item ).children();
+ if ( backwards ) {
+ children = $( children.get().reverse() );
+ }
+ return checkAndGo( children, findNextInChildren, selector, backwards );
+ }
+ function findNextInSiblings( item, selector, backwards ) {
+ return checkAndGo(
+ $( item )[ backwards ? 'prevAll' : 'nextAll' ](),
+ findNextInChildren,
+ selector,
+ backwards
+ );
+ }
+ function findNextInParents( item, selector, backwards ) {
+ var next;
+ var parents = $( item ).parents();
+ parents = $( parents.get() );
+ $.each( parents.get(), function( idx, element ) {
+ if ( backwards && $( element ).is( selector ) ) {
+ next = element;
+ return false;
+ }
+ next = findNextInSiblings( element, selector, backwards );
+ if ( next ) {
+ return false;
+ }
+ } );
+ return next;
+ }
+
+ $.fn.near = function( selector, backwards ) {
+ var array = [];
+ $( this ).each( function( idx, element ) {
+ var near =
+ ( backwards ? false : findNextInChildren( element, selector, backwards ) ) ||
+ findNextInSiblings( element, selector, backwards ) ||
+ findNextInParents( element, selector, backwards );
+ if ( near ) {
+ array.push( near );
+ }
+ } );
+ return $( array );
+ };
+} )( jQuery, document, window );
+/*
+ * transform.js
+ * The engine that powers the transforms or falls back to other methods
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+
+ /* FUNCTIONS */
+ function toCssNumber( number ) {
+ return Math.round( 10000 * number ) / 10000 + '';
+ }
+
+ /**
+ * 3D and 2D engines
+ */
+ var engines = {
+ 3: {
+ transform: function( el, data ) {
+ var transform = 'translate(-50%,-50%)';
+ $.each( data, function( idx, item ) {
+ var coord = [ 'X', 'Y', 'Z' ];
+ var i;
+ if ( item[ 0 ] === 'translate' ) {
+ // ["translate", x, y, z]
+ transform +=
+ ' translate3d(' +
+ toCssNumber( item[ 1 ] || 0 ) +
+ 'px,' +
+ toCssNumber( item[ 2 ] || 0 ) +
+ 'px,' +
+ toCssNumber( item[ 3 ] || 0 ) +
+ 'px)';
+ } else if ( item[ 0 ] === 'rotate' ) {
+ var order = item[ 4 ] ? [ 1, 2, 3 ] : [ 3, 2, 1 ];
+ for ( i = 0; i < 3; i++ ) {
+ transform +=
+ ' rotate' +
+ coord[ order[ i ] - 1 ] +
+ '(' +
+ toCssNumber( item[ order[ i ] ] || 0 ) +
+ 'deg)';
+ }
+ } else if ( item[ 0 ] === 'scale' ) {
+ for ( i = 0; i < 3; i++ ) {
+ transform += ' scale' + coord[ i ] + '(' + toCssNumber( item[ i + 1 ] || 1 ) + ')';
+ }
+ }
+ } );
+ $.jmpress( 'css', el, $.extend( {}, { transform: transform } ) );
+ },
+ },
+ 2: {
+ transform: function( el, data ) {
+ var transform = 'translate(-50%,-50%)';
+ $.each( data, function( idx, item ) {
+ var coord = [ 'X', 'Y' ];
+ if ( item[ 0 ] === 'translate' ) {
+ // ["translate", x, y, z]
+ transform +=
+ ' translate(' +
+ toCssNumber( item[ 1 ] || 0 ) +
+ 'px,' +
+ toCssNumber( item[ 2 ] || 0 ) +
+ 'px)';
+ } else if ( item[ 0 ] === 'rotate' ) {
+ transform += ' rotate(' + toCssNumber( item[ 3 ] || 0 ) + 'deg)';
+ } else if ( item[ 0 ] === 'scale' ) {
+ for ( var i = 0; i < 2; i++ ) {
+ transform += ' scale' + coord[ i ] + '(' + toCssNumber( item[ i + 1 ] || 1 ) + ')';
+ }
+ }
+ } );
+ $.jmpress( 'css', el, $.extend( {}, { transform: transform } ) );
+ },
+ },
+ 1: {
+ // CHECK IF SUPPORT IS REALLY NEEDED?
+ // this not even work without scaling...
+ // it may better to display the normal view
+ transform: function( el, data ) {
+ var anitarget = { top: 0, left: 0 };
+ $.each( data, function( idx, item ) {
+ var coord = [ 'X', 'Y' ];
+ if ( item[ 0 ] === 'translate' ) {
+ // ["translate", x, y, z]
+ anitarget.left = Math.round( item[ 1 ] || 0 ) + 'px';
+ anitarget.top = Math.round( item[ 2 ] || 0 ) + 'px';
+ }
+ } );
+ el.animate( anitarget, 1000 ); // TODO: Use animation duration
+ },
+ },
+ };
+
+ /**
+ * Engine to power cross-browser translate, scale and rotate.
+ */
+ var engine = ( function() {
+ if ( $.jmpress( 'pfx', 'perspective' ) ) {
+ return engines[ 3 ];
+ } else if ( $.jmpress( 'pfx', 'transform' ) ) {
+ return engines[ 2 ];
+ } else {
+ // CHECK IF SUPPORT IS REALLY NEEDED?
+ return engines[ 1 ];
+ }
+ } )();
+
+ $.jmpress( 'defaults' ).reasonableAnimation = {};
+ $.jmpress( 'initStep', function( step, eventData ) {
+ var data = eventData.data;
+ var stepData = eventData.stepData;
+ var pf = parseFloat;
+ $.extend( stepData, {
+ x: pf( data.x ) || 0,
+ y: pf( data.y ) || 0,
+ z: pf( data.z ) || 0,
+ r: pf( data.r ) || 0,
+ phi: pf( data.phi ) || 0,
+ rotate: pf( data.rotate ) || 0,
+ rotateX: pf( data.rotateX ) || 0,
+ rotateY: pf( data.rotateY ) || 0,
+ rotateZ: pf( data.rotateZ ) || 0,
+ revertRotate: false,
+ scale: pf( data.scale ) || 1,
+ scaleX: pf( data.scaleX ) || false,
+ scaleY: pf( data.scaleY ) || false,
+ scaleZ: pf( data.scaleZ ) || 1,
+ } );
+ } );
+ $.jmpress( 'afterInit', function( nil, eventData ) {
+ var stepSelector = eventData.settings.stepSelector,
+ current = eventData.current;
+ current.perspectiveScale = 1;
+ current.maxNestedDepth = 0;
+ var nestedSteps = $( eventData.jmpress )
+ .find( stepSelector )
+ .children( stepSelector );
+ while ( nestedSteps.length ) {
+ current.maxNestedDepth++;
+ nestedSteps = nestedSteps.children( stepSelector );
+ }
+ } );
+ $.jmpress( 'applyStep', function( step, eventData ) {
+ $.jmpress( 'css', $( step ), {
+ position: 'absolute',
+ transformStyle: 'preserve-3d',
+ } );
+ if ( eventData.parents.length > 0 ) {
+ $.jmpress( 'css', $( step ), {
+ top: '50%',
+ left: '50%',
+ } );
+ }
+ var sd = eventData.stepData;
+ var transform = [
+ [
+ 'translate',
+ sd.x || sd.r * Math.sin( ( sd.phi * Math.PI ) / 180 ),
+ sd.y || -sd.r * Math.cos( ( sd.phi * Math.PI ) / 180 ),
+ sd.z,
+ ],
+ [ 'rotate', sd.rotateX, sd.rotateY, sd.rotateZ || sd.rotate, true ],
+ [ 'scale', sd.scaleX || sd.scale, sd.scaleY || sd.scale, sd.scaleZ || sd.scale ],
+ ];
+ engine.transform( step, transform );
+ } );
+ $.jmpress( 'setActive', function( element, eventData ) {
+ var target = eventData.target;
+ var step = eventData.stepData;
+ var tf = ( target.transform = [] );
+ target.perspectiveScale = 1;
+
+ for ( var i = eventData.current.maxNestedDepth; i > ( eventData.parents.length || 0 ); i-- ) {
+ tf.push( [ 'scale' ], [ 'rotate' ], [ 'translate' ] );
+ }
+
+ tf.push( [
+ 'scale',
+ 1 / ( step.scaleX || step.scale ),
+ 1 / ( step.scaleY || step.scale ),
+ 1 / step.scaleZ,
+ ] );
+ tf.push( [ 'rotate', -step.rotateX, -step.rotateY, -( step.rotateZ || step.rotate ) ] );
+ tf.push( [
+ 'translate',
+ -( step.x || step.r * Math.sin( ( step.phi * Math.PI ) / 180 ) ),
+ -( step.y || -step.r * Math.cos( ( step.phi * Math.PI ) / 180 ) ),
+ -step.z,
+ ] );
+ target.perspectiveScale *= step.scaleX || step.scale;
+
+ $.each( eventData.parents, function( idx, element ) {
+ var step = $( element ).data( 'stepData' );
+ tf.push( [
+ 'scale',
+ 1 / ( step.scaleX || step.scale ),
+ 1 / ( step.scaleY || step.scale ),
+ 1 / step.scaleZ,
+ ] );
+ tf.push( [ 'rotate', -step.rotateX, -step.rotateY, -( step.rotateZ || step.rotate ) ] );
+ tf.push( [
+ 'translate',
+ -( step.x || step.r * Math.sin( ( step.phi * Math.PI ) / 180 ) ),
+ -( step.y || -step.r * Math.cos( ( step.phi * Math.PI ) / 180 ) ),
+ -step.z,
+ ] );
+ target.perspectiveScale *= step.scaleX || step.scale;
+ } );
+
+ $.each( tf, function( idx, item ) {
+ if ( item[ 0 ] !== 'rotate' ) {
+ return;
+ }
+ function lowRotate( name ) {
+ if ( eventData.current[ 'rotate' + name + '-' + idx ] === undefined ) {
+ eventData.current[ 'rotate' + name + '-' + idx ] = item[ name ] || 0;
+ }
+ var cur = eventData.current[ 'rotate' + name + '-' + idx ],
+ tar = item[ name ] || 0,
+ curmod = cur % 360,
+ tarmod = tar % 360;
+ if ( curmod < 0 ) {
+ curmod += 360;
+ }
+ if ( tarmod < 0 ) {
+ tarmod += 360;
+ }
+ var diff = tarmod - curmod;
+ if ( diff < -180 ) {
+ diff += 360;
+ } else if ( diff > 180 ) {
+ diff -= 360;
+ }
+ eventData.current[ 'rotate' + name + '-' + idx ] = item[ name ] = cur + diff;
+ }
+ lowRotate( 1 );
+ lowRotate( 2 );
+ lowRotate( 3 );
+ } );
+ } );
+ $.jmpress( 'applyTarget', function( active, eventData ) {
+ var target = eventData.target,
+ props,
+ step = eventData.stepData,
+ settings = eventData.settings,
+ zoomin = target.perspectiveScale * 1.3 < eventData.current.perspectiveScale,
+ zoomout = target.perspectiveScale > eventData.current.perspectiveScale * 1.3;
+
+ // extract first scale from transform
+ var lastScale = -1;
+ $.each( target.transform, function( idx, item ) {
+ if ( item.length <= 1 ) {
+ return;
+ }
+ if (
+ item[ 0 ] === 'rotate' &&
+ item[ 1 ] % 360 === 0 &&
+ item[ 2 ] % 360 === 0 &&
+ item[ 3 ] % 360 === 0
+ ) {
+ return;
+ }
+ if ( item[ 0 ] === 'scale' ) {
+ lastScale = idx;
+ } else {
+ return false;
+ }
+ } );
+
+ if ( lastScale !== eventData.current.oldLastScale ) {
+ zoomin = zoomout = false;
+ eventData.current.oldLastScale = lastScale;
+ }
+
+ var extracted = [];
+ if ( lastScale !== -1 ) {
+ while ( lastScale >= 0 ) {
+ if ( target.transform[ lastScale ][ 0 ] === 'scale' ) {
+ extracted.push( target.transform[ lastScale ] );
+ target.transform[ lastScale ] = [ 'scale' ];
+ }
+ lastScale--;
+ }
+ }
+
+ var animation = settings.animation;
+ if ( settings.reasonableAnimation[ eventData.reason ] ) {
+ animation = $.extend( {}, animation, settings.reasonableAnimation[ eventData.reason ] );
+ }
+
+ props = {
+ // to keep the perspective look similar for different scales
+ // we need to 'scale' the perspective, too
+ perspective: Math.round( target.perspectiveScale * 1000 ) + 'px',
+ };
+ props = $.extend( {}, animation, props );
+ if ( ! zoomin ) {
+ props.transitionDelay = '0s';
+ }
+ if ( ! eventData.beforeActive ) {
+ props.transitionDuration = '0s';
+ props.transitionDelay = '0s';
+ }
+ $.jmpress( 'css', eventData.area, props );
+ engine.transform( eventData.area, extracted );
+
+ props = $.extend( {}, animation );
+ if ( ! zoomout ) {
+ props.transitionDelay = '0s';
+ }
+ if ( ! eventData.beforeActive ) {
+ props.transitionDuration = '0s';
+ props.transitionDelay = '0s';
+ }
+
+ eventData.current.perspectiveScale = target.perspectiveScale;
+
+ $.jmpress( 'css', eventData.canvas, props );
+ engine.transform( eventData.canvas, target.transform );
+ } );
+} )( jQuery, document, window );
+/*
+ * active.js
+ * Set the active classes on steps
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ var $jmpress = $.jmpress;
+
+ /* DEFINES */
+ var activeClass = 'activeClass',
+ nestedActiveClass = 'nestedActiveClass';
+
+ /* DEFAULTS */
+ var defaults = $jmpress( 'defaults' );
+ defaults[ nestedActiveClass ] = 'nested-active';
+ defaults[ activeClass ] = 'active';
+
+ /* HOOKS */
+ $jmpress( 'setInactive', function( step, eventData ) {
+ var settings = eventData.settings,
+ activeClassSetting = settings[ activeClass ],
+ nestedActiveClassSettings = settings[ nestedActiveClass ];
+ if ( activeClassSetting ) {
+ $( step ).removeClass( activeClassSetting );
+ }
+ if ( nestedActiveClassSettings ) {
+ $.each( eventData.parents, function( idx, element ) {
+ $( element ).removeClass( nestedActiveClassSettings );
+ } );
+ }
+ } );
+ $jmpress( 'setActive', function( step, eventData ) {
+ var settings = eventData.settings,
+ activeClassSetting = settings[ activeClass ],
+ nestedActiveClassSettings = settings[ nestedActiveClass ];
+ if ( activeClassSetting ) {
+ $( step ).addClass( activeClassSetting );
+ }
+ if ( nestedActiveClassSettings ) {
+ $.each( eventData.parents, function( idx, element ) {
+ $( element ).addClass( nestedActiveClassSettings );
+ } );
+ }
+ } );
+} )( jQuery, document, window );
+/*
+ * circular.js
+ * Repeat from start after end
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ var $jmpress = $.jmpress;
+
+ /* FUNCTIONS */
+ function firstSlide( step, eventData ) {
+ return $( this )
+ .find( eventData.settings.stepSelector )
+ .first();
+ }
+ function prevOrNext( jmpress, step, eventData, prev ) {
+ if ( ! step ) {
+ return false;
+ }
+ var stepSelector = eventData.settings.stepSelector;
+ step = $( step );
+ do {
+ var item = step.near( stepSelector, prev );
+ if ( item.length === 0 || item.closest( jmpress ).length === 0 ) {
+ item = $( jmpress )
+ .find( stepSelector )
+ [ prev ? 'last' : 'first' ]();
+ }
+ if ( ! item.length ) {
+ return false;
+ }
+ step = item;
+ } while ( step.data( 'stepData' ).exclude );
+ return step;
+ }
+
+ /* HOOKS */
+ $jmpress( 'initStep', function( step, eventData ) {
+ eventData.stepData.exclude =
+ eventData.data.exclude && [ 'false', 'no' ].indexOf( eventData.data.exclude ) === -1;
+ } );
+ $jmpress( 'selectInitialStep', firstSlide );
+ $jmpress( 'selectHome', firstSlide );
+ $jmpress( 'selectEnd', function( step, eventData ) {
+ return $( this )
+ .find( eventData.settings.stepSelector )
+ .last();
+ } );
+ $jmpress( 'selectPrev', function( step, eventData ) {
+ return prevOrNext( this, step, eventData, true );
+ } );
+ $jmpress( 'selectNext', function( step, eventData ) {
+ return prevOrNext( this, step, eventData );
+ } );
+} )( jQuery, document, window );
+/*
+ * start.js
+ * Set the first step to start on
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+
+ /* HOOKS */
+ $.jmpress( 'selectInitialStep', function( nil, eventData ) {
+ return eventData.settings.start;
+ } );
+} )( jQuery, document, window );
+/*
+ * ways.js
+ * Control the flow of the steps
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ var $jmpress = $.jmpress;
+
+ /* FUNCTIONS */
+ function routeFunc( jmpress, route, type ) {
+ for ( var i = 0; i < route.length - 1; i++ ) {
+ var from = route[ i ];
+ var to = route[ i + 1 ];
+ if ( $( jmpress ).jmpress( 'initialized' ) ) {
+ $( from, jmpress ).data( 'stepData' )[ type ] = to;
+ } else {
+ $( from, jmpress ).attr( 'data-' + type, to );
+ }
+ }
+ }
+ function selectPrevOrNext( step, eventData, attr, prev ) {
+ var stepData = eventData.stepData;
+ if ( stepData[ attr ] ) {
+ var near = $( step ).near( stepData[ attr ], prev );
+ if ( near && near.length ) {
+ return near;
+ }
+ near = $( stepData[ attr ], this )[ prev ? 'last' : 'first' ]();
+ if ( near && near.length ) {
+ return near;
+ }
+ }
+ }
+
+ /* EXPORTED FUNCTIONS */
+ $jmpress( 'register', 'route', function( route, unidirectional, reversedRoute ) {
+ if ( typeof route === 'string' ) {
+ route = [ route, route ];
+ }
+ routeFunc( this, route, reversedRoute ? 'prev' : 'next' );
+ if ( ! unidirectional ) {
+ routeFunc( this, route.reverse(), reversedRoute ? 'next' : 'prev' );
+ }
+ } );
+
+ /* HOOKS */
+ $jmpress( 'initStep', function( step, eventData ) {
+ for ( var attr in { next: 1, prev: 1 } ) {
+ eventData.stepData[ attr ] = eventData.data[ attr ];
+ }
+ } );
+ $jmpress( 'selectNext', function( step, eventData ) {
+ return selectPrevOrNext.call( this, step, eventData, 'next' );
+ } );
+ $jmpress( 'selectPrev', function( step, eventData ) {
+ return selectPrevOrNext.call( this, step, eventData, 'prev', true );
+ } );
+} )( jQuery, document, window );
+/*
+ * ajax.js
+ * Load steps via ajax
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ var $jmpress = $.jmpress;
+
+ /* DEFINES */
+ var afterStepLoaded = 'ajax:afterStepLoaded',
+ loadStep = 'ajax:loadStep';
+
+ /* REGISTER EVENTS */
+ $jmpress( 'register', loadStep );
+ $jmpress( 'register', afterStepLoaded );
+
+ /* DEFAULTS */
+ $jmpress( 'defaults' ).ajaxLoadedClass = 'loaded';
+
+ /* HOOKS */
+ $jmpress( 'initStep', function( step, eventData ) {
+ eventData.stepData.src = $( step ).attr( 'href' ) || eventData.data.src || false;
+ eventData.stepData.srcLoaded = false;
+ } );
+ $jmpress( loadStep, function( step, eventData ) {
+ var stepData = eventData.stepData,
+ href = stepData && stepData.src,
+ settings = eventData.settings;
+ if ( href ) {
+ $( step ).addClass( settings.ajaxLoadedClass );
+ stepData.srcLoaded = true;
+ $( step ).load( href, function( response, status, xhr ) {
+ $( eventData.jmpress ).jmpress(
+ 'fire',
+ afterStepLoaded,
+ step,
+ $.extend( {}, eventData, {
+ response: response,
+ status: status,
+ xhr: xhr,
+ } )
+ );
+ } );
+ }
+ } );
+ $jmpress( 'idle', function( step, eventData ) {
+ if ( ! step ) {
+ return;
+ }
+ var settings = eventData.settings,
+ jmpress = $( this ),
+ stepData = eventData.stepData;
+ var siblings = $( step )
+ .add( $( step ).near( settings.stepSelector ) )
+ .add( $( step ).near( settings.stepSelector, true ) )
+ .add(
+ jmpress.jmpress( 'fire', 'selectPrev', step, {
+ stepData: $( step ).data( 'stepData' ),
+ } )
+ )
+ .add(
+ jmpress.jmpress( 'fire', 'selectNext', step, {
+ stepData: $( step ).data( 'stepData' ),
+ } )
+ );
+ siblings.each( function() {
+ var step = this,
+ stepData = $( step ).data( 'stepData' );
+ if ( ! stepData.src || stepData.srcLoaded ) {
+ return;
+ }
+ jmpress.jmpress( 'fire', loadStep, step, {
+ stepData: $( step ).data( 'stepData' ),
+ } );
+ } );
+ } );
+ $jmpress( 'setActive', function( step, eventData ) {
+ var stepData = $( step ).data( 'stepData' );
+ if ( ! stepData.src || stepData.srcLoaded ) {
+ return;
+ }
+ $( this ).jmpress( 'fire', loadStep, step, {
+ stepData: $( step ).data( 'stepData' ),
+ } );
+ } );
+} )( jQuery, document, window );
+/*
+ * hash.js
+ * Detect and set the URL hash
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ var $jmpress = $.jmpress,
+ hashLink = "a[href^='#']";
+
+ /* FUNCTIONS */
+ function randomString() {
+ return '' + Math.round( Math.random() * 100000, 0 );
+ }
+ /**
+ * getElementFromUrl
+ *
+ * @return String or undefined
+ */
+ function getElementFromUrl( settings ) {
+ // get id from url # by removing `#` or `#/` from the beginning,
+ // so both "fallback" `#slide-id` and "enhanced" `#/slide-id` will work
+ // TODO SECURITY check user input to be valid!
+ try {
+ var el = $( '#' + window.location.hash.replace( /^#\/?/, '' ) );
+ return el.length > 0 && el.is( settings.stepSelector ) ? el : undefined;
+ } catch ( e ) {}
+ }
+ function setHash( stepid ) {
+ var shouldBeHash = '#/' + stepid;
+ if ( window.history && window.history.pushState ) {
+ // shouldBeHash = "#" + stepid;
+ // consider this for future versions
+ // it has currently issues, when startup with a link with hash (webkit)
+ if ( window.location.hash !== shouldBeHash ) {
+ window.history.pushState( {}, '', shouldBeHash );
+ }
+ } else {
+ if ( window.location.hash !== shouldBeHash ) {
+ window.location.hash = shouldBeHash;
+ }
+ }
+ }
+
+ /* DEFAULTS */
+ $jmpress( 'defaults' ).hash = {
+ use: true,
+ update: true,
+ bindChange: true,
+ // NOTICE: {use: true, update: false, bindChange: true}
+ // will cause a error after clicking on a link to the current step
+ };
+
+ /* HOOKS */
+ $jmpress( 'selectInitialStep', function( step, eventData ) {
+ var settings = eventData.settings,
+ hashSettings = settings.hash,
+ current = eventData.current,
+ jmpress = $( this );
+ eventData.current.hashNamespace = '.jmpress-' + randomString();
+ // HASH CHANGE EVENT
+ if ( hashSettings.use ) {
+ if ( hashSettings.bindChange ) {
+ $( window ).bind( 'hashchange' + current.hashNamespace, function( event ) {
+ var urlItem = getElementFromUrl( settings );
+ if ( jmpress.jmpress( 'initialized' ) ) {
+ jmpress.jmpress( 'scrollFix' );
+ }
+ if ( urlItem && urlItem.length ) {
+ if ( urlItem.attr( 'id' ) !== jmpress.jmpress( 'active' ).attr( 'id' ) ) {
+ jmpress.jmpress( 'select', urlItem );
+ }
+ setHash( urlItem.attr( 'id' ) );
+ }
+ event.preventDefault();
+ } );
+ $( hashLink ).on( 'click' + current.hashNamespace, function( event ) {
+ var href = $( this ).attr( 'href' );
+ try {
+ if ( $( href ).is( settings.stepSelector ) ) {
+ jmpress.jmpress( 'select', href );
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ } catch ( e ) {}
+ } );
+ }
+ return getElementFromUrl( settings );
+ }
+ } );
+ $jmpress( 'afterDeinit', function( nil, eventData ) {
+ $( hashLink ).off( eventData.current.hashNamespace );
+ $( window ).unbind( eventData.current.hashNamespace );
+ } );
+ $jmpress( 'setActive', function( step, eventData ) {
+ var settings = eventData.settings,
+ current = eventData.current;
+ // `#/step-id` is used instead of `#step-id` to prevent default browser
+ // scrolling to element in hash
+ if ( settings.hash.use && settings.hash.update ) {
+ clearTimeout( current.hashtimeout );
+ current.hashtimeout = setTimeout( function() {
+ setHash( $( eventData.delegatedFrom ).attr( 'id' ) );
+ }, settings.transitionDuration + 200 );
+ }
+ } );
+} )( jQuery, document, window );
+/*
+ * keyboard.js
+ * Keyboard event mapping and default keyboard actions
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ var $jmpress = $.jmpress,
+ jmpressNext = 'next',
+ jmpressPrev = 'prev';
+
+ /* FUNCTIONS */
+ function randomString() {
+ return '' + Math.round( Math.random() * 100000, 0 );
+ }
+ function stopEvent( event ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ /* DEFAULTS */
+ $jmpress( 'defaults' ).keyboard = {
+ use: true,
+ keys: {
+ 33: jmpressPrev, // pg up
+ 37: jmpressPrev, // left
+ 38: jmpressPrev, // up
+
+ 9: jmpressNext + ':' + jmpressPrev, // tab
+ 32: jmpressNext, // space
+ 34: jmpressNext, // pg down
+ 39: jmpressNext, // right
+ 40: jmpressNext, // down
+
+ 36: 'home', // home
+
+ 35: 'end', // end
+ },
+ ignore: {
+ INPUT: [
+ 32, // space
+ 37, // left
+ 38, // up
+ 39, // right
+ 40, // down
+ ],
+ TEXTAREA: [
+ 32, // space
+ 37, // left
+ 38, // up
+ 39, // right
+ 40, // down
+ ],
+ SELECT: [
+ 38, // up
+ 40, // down
+ ],
+ },
+ tabSelector: 'a[href]:visible, :input:visible',
+ };
+
+ /* HOOKS */
+ $jmpress( 'afterInit', function( nil, eventData ) {
+ var settings = eventData.settings,
+ keyboardSettings = settings.keyboard,
+ ignoreKeyboardSettings = keyboardSettings.ignore,
+ current = eventData.current,
+ jmpress = $( this );
+
+ // tabindex make it focusable so that it can receive key events
+ if ( ! settings.fullscreen ) {
+ jmpress.attr( 'tabindex', 0 );
+ }
+
+ current.keyboardNamespace = '.jmpress-' + randomString();
+
+ // KEYPRESS EVENT: this fixes a Opera bug
+ $( settings.fullscreen ? document : jmpress ).bind(
+ 'keypress' + current.keyboardNamespace,
+ function( event ) {
+ for ( var nodeName in ignoreKeyboardSettings ) {
+ if (
+ event.target.nodeName === nodeName &&
+ ignoreKeyboardSettings[ nodeName ].indexOf( event.which ) !== -1
+ ) {
+ return;
+ }
+ }
+ if ( ( event.which >= 37 && event.which <= 40 ) || event.which === 32 ) {
+ stopEvent( event );
+ }
+ }
+ );
+ // KEYDOWN EVENT
+ $( settings.fullscreen ? document : jmpress ).bind(
+ 'keydown' + current.keyboardNamespace,
+ function( event ) {
+ var eventTarget = $( event.target );
+
+ if (
+ ( ! settings.fullscreen && ! eventTarget.closest( jmpress ).length ) ||
+ ! keyboardSettings.use
+ ) {
+ return;
+ }
+
+ for ( var nodeName in ignoreKeyboardSettings ) {
+ if (
+ eventTarget[ 0 ].nodeName === nodeName &&
+ ignoreKeyboardSettings[ nodeName ].indexOf( event.which ) !== -1
+ ) {
+ return;
+ }
+ }
+
+ var reverseSelect = false;
+ var nextFocus;
+ if ( event.which === 9 ) {
+ // tab
+ if ( ! eventTarget.closest( jmpress.jmpress( 'active' ) ).length ) {
+ if ( ! event.shiftKey ) {
+ nextFocus = jmpress
+ .jmpress( 'active' )
+ .find( 'a[href], :input' )
+ .filter( ':visible' )
+ .first();
+ } else {
+ reverseSelect = true;
+ }
+ } else {
+ nextFocus = eventTarget.near( keyboardSettings.tabSelector, event.shiftKey );
+ if (
+ ! $( nextFocus )
+ .closest( settings.stepSelector )
+ .is( jmpress.jmpress( 'active' ) )
+ ) {
+ nextFocus = undefined;
+ }
+ }
+ if ( nextFocus && nextFocus.length > 0 ) {
+ nextFocus.focus();
+ jmpress.jmpress( 'scrollFix' );
+ stopEvent( event );
+ return;
+ } else {
+ if ( event.shiftKey ) {
+ reverseSelect = true;
+ }
+ }
+ }
+
+ var action = keyboardSettings.keys[ event.which ];
+ if ( typeof action === 'string' ) {
+ if ( action.indexOf( ':' ) !== -1 ) {
+ action = action.split( ':' );
+ action = event.shiftKey ? action[ 1 ] : action[ 0 ];
+ }
+ jmpress.jmpress( action );
+ stopEvent( event );
+ } else if ( $.isFunction( action ) ) {
+ action.call( jmpress, event );
+ } else if ( action ) {
+ jmpress.jmpress.apply( jmpress, action );
+ stopEvent( event );
+ }
+
+ if ( reverseSelect ) {
+ // tab
+ nextFocus = jmpress
+ .jmpress( 'active' )
+ .find( 'a[href], :input' )
+ .filter( ':visible' )
+ .last();
+ nextFocus.focus();
+ jmpress.jmpress( 'scrollFix' );
+ }
+ }
+ );
+ } );
+ $jmpress( 'afterDeinit', function( nil, eventData ) {
+ $( document ).unbind( eventData.current.keyboardNamespace );
+ } );
+} )( jQuery, document, window );
+/*
+ * viewport.js
+ * Scale to fit a given viewport
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+
+ function randomString() {
+ return '' + Math.round( Math.random() * 100000, 0 );
+ }
+
+ var browser = ( function() {
+ var ua = navigator.userAgent.toLowerCase();
+ var match =
+ /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
+ /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
+ /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
+ /(msie) ([\w.]+)/.exec( ua ) ||
+ ( ua.indexOf( 'compatible' ) < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ) ||
+ [];
+ return match[ 1 ] || '';
+ } )();
+
+ var defaults = $.jmpress( 'defaults' );
+ defaults.viewPort = {
+ width: false,
+ height: false,
+ maxScale: 0,
+ minScale: 0,
+ zoomable: 0,
+ zoomBindMove: true,
+ zoomBindWheel: true,
+ };
+ var keys = defaults.keyboard.keys;
+ keys[ browser === 'mozilla' ? 107 : 187 ] = 'zoomIn'; // +
+ keys[ browser === 'mozilla' ? 109 : 189 ] = 'zoomOut'; // -
+ defaults.reasonableAnimation.resize = {
+ transitionDuration: '0s',
+ transitionDelay: '0ms',
+ };
+ defaults.reasonableAnimation.zoom = {
+ transitionDuration: '0s',
+ transitionDelay: '0ms',
+ };
+ $.jmpress( 'initStep', function( step, eventData ) {
+ for ( var variable in {
+ viewPortHeight: 1,
+ viewPortWidth: 1,
+ viewPortMinScale: 1,
+ viewPortMaxScale: 1,
+ viewPortZoomable: 1,
+ } ) {
+ eventData.stepData[ variable ] =
+ eventData.data[ variable ] && parseFloat( eventData.data[ variable ] );
+ }
+ } );
+ $.jmpress( 'afterInit', function( nil, eventData ) {
+ var jmpress = this;
+ eventData.current.viewPortNamespace = '.jmpress-' + randomString();
+ $( window ).bind( 'resize' + eventData.current.viewPortNamespace, function( event ) {
+ $( jmpress ).jmpress( 'reselect', 'resize' );
+ } );
+ eventData.current.userZoom = 0;
+ eventData.current.userTranslateX = 0;
+ eventData.current.userTranslateY = 0;
+ if ( eventData.settings.viewPort.zoomBindWheel ) {
+ $( eventData.settings.fullscreen ? document : this ).bind(
+ 'mousewheel' +
+ eventData.current.viewPortNamespace +
+ ' DOMMouseScroll' +
+ eventData.current.viewPortNamespace,
+ function( event, delta ) {
+ delta =
+ delta || event.originalEvent.wheelDelta || -event.originalEvent.detail /* mozilla */;
+ var direction = delta / Math.abs( delta );
+ if ( direction < 0 ) {
+ $( eventData.jmpress ).jmpress(
+ 'zoomOut',
+ event.originalEvent.x,
+ event.originalEvent.y
+ );
+ } else if ( direction > 0 ) {
+ $( eventData.jmpress ).jmpress(
+ 'zoomIn',
+ event.originalEvent.x,
+ event.originalEvent.y
+ );
+ }
+ return false;
+ }
+ );
+ }
+ if ( eventData.settings.viewPort.zoomBindMove ) {
+ $( eventData.settings.fullscreen ? document : this )
+ .bind( 'mousedown' + eventData.current.viewPortNamespace, function( event ) {
+ if ( eventData.current.userZoom ) {
+ eventData.current.userTranslating = { x: event.clientX, y: event.clientY };
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+ } )
+ .bind( 'mousemove' + eventData.current.viewPortNamespace, function( event ) {
+ var userTranslating = eventData.current.userTranslating;
+ if ( userTranslating ) {
+ $( jmpress ).jmpress(
+ 'zoomTranslate',
+ event.clientX - userTranslating.x,
+ event.clientY - userTranslating.y
+ );
+ userTranslating.x = event.clientX;
+ userTranslating.y = event.clientY;
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+ } )
+ .bind( 'mouseup' + eventData.current.viewPortNamespace, function( event ) {
+ if ( eventData.current.userTranslating ) {
+ eventData.current.userTranslating = undefined;
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+ } );
+ }
+ } );
+ function maxAbs( value, range ) {
+ return Math.max( Math.min( value, range ), -range );
+ }
+ function zoom( x, y, direction ) {
+ var current = $( this ).jmpress( 'current' ),
+ settings = $( this ).jmpress( 'settings' ),
+ stepData = $( this )
+ .jmpress( 'active' )
+ .data( 'stepData' ),
+ container = $( this ).jmpress( 'container' );
+ if ( current.userZoom === 0 && direction < 0 ) {
+ return;
+ }
+ var zoomableSteps = stepData.viewPortZoomable || settings.viewPort.zoomable;
+ if ( current.userZoom === zoomableSteps && direction > 0 ) {
+ return;
+ }
+ current.userZoom += direction;
+
+ var halfWidth = $( container ).innerWidth() / 2,
+ halfHeight = $( container ).innerHeight() / 2;
+
+ x = x ? x - halfWidth : x;
+ y = y ? y - halfHeight : y;
+
+ // TODO this is not perfect... too much math... :(
+ current.userTranslateX = maxAbs(
+ current.userTranslateX - ( direction * x ) / current.zoomOriginWindowScale / zoomableSteps,
+ ( halfWidth * current.userZoom * current.userZoom ) / zoomableSteps
+ );
+ current.userTranslateY = maxAbs(
+ current.userTranslateY - ( direction * y ) / current.zoomOriginWindowScale / zoomableSteps,
+ ( halfHeight * current.userZoom * current.userZoom ) / zoomableSteps
+ );
+
+ $( this ).jmpress( 'reselect', 'zoom' );
+ }
+ $.jmpress( 'register', 'zoomIn', function( x, y ) {
+ zoom.call( this, x || 0, y || 0, 1 );
+ } );
+ $.jmpress( 'register', 'zoomOut', function( x, y ) {
+ zoom.call( this, x || 0, y || 0, -1 );
+ } );
+ $.jmpress( 'register', 'zoomTranslate', function( x, y ) {
+ var current = $( this ).jmpress( 'current' ),
+ settings = $( this ).jmpress( 'settings' ),
+ stepData = $( this )
+ .jmpress( 'active' )
+ .data( 'stepData' ),
+ container = $( this ).jmpress( 'container' );
+ var zoomableSteps = stepData.viewPortZoomable || settings.viewPort.zoomable;
+ var halfWidth = $( container ).innerWidth(),
+ halfHeight = $( container ).innerHeight();
+ current.userTranslateX = maxAbs(
+ current.userTranslateX + x / current.zoomOriginWindowScale,
+ ( halfWidth * current.userZoom * current.userZoom ) / zoomableSteps
+ );
+ current.userTranslateY = maxAbs(
+ current.userTranslateY + y / current.zoomOriginWindowScale,
+ ( halfHeight * current.userZoom * current.userZoom ) / zoomableSteps
+ );
+ $( this ).jmpress( 'reselect', 'zoom' );
+ } );
+ $.jmpress( 'afterDeinit', function( nil, eventData ) {
+ $( eventData.settings.fullscreen ? document : this ).unbind(
+ eventData.current.viewPortNamespace
+ );
+ $( window ).unbind( eventData.current.viewPortNamespace );
+ } );
+ $.jmpress( 'setActive', function( step, eventData ) {
+ var viewPort = eventData.settings.viewPort;
+ var viewPortHeight = eventData.stepData.viewPortHeight || viewPort.height;
+ var viewPortWidth = eventData.stepData.viewPortWidth || viewPort.width;
+ var viewPortMaxScale = eventData.stepData.viewPortMaxScale || viewPort.maxScale;
+ var viewPortMinScale = eventData.stepData.viewPortMinScale || viewPort.minScale;
+ // Correct the scale based on the window's size
+ var windowScaleY = viewPortHeight && $( eventData.container ).innerHeight() / viewPortHeight;
+ var windowScaleX = viewPortWidth && $( eventData.container ).innerWidth() / viewPortWidth;
+ var windowScale =
+ ( windowScaleX || windowScaleY ) &&
+ Math.min( windowScaleX || windowScaleY, windowScaleY || windowScaleX );
+
+ if ( windowScale ) {
+ windowScale = windowScale || 1;
+ if ( viewPortMaxScale ) {
+ windowScale = Math.min( windowScale, viewPortMaxScale );
+ }
+ if ( viewPortMinScale ) {
+ windowScale = Math.max( windowScale, viewPortMinScale );
+ }
+
+ var zoomableSteps =
+ eventData.stepData.viewPortZoomable || eventData.settings.viewPort.zoomable;
+ if ( zoomableSteps ) {
+ var diff = 1 / windowScale - 1 / viewPortMaxScale;
+ diff /= zoomableSteps;
+ windowScale = 1 / ( 1 / windowScale - diff * eventData.current.userZoom );
+ }
+
+ eventData.target.transform.reverse();
+ if ( eventData.current.userTranslateX && eventData.current.userTranslateY ) {
+ eventData.target.transform.push( [
+ 'translate',
+ eventData.current.userTranslateX,
+ eventData.current.userTranslateY,
+ 0,
+ ] );
+ } else {
+ eventData.target.transform.push( [ 'translate' ] );
+ }
+ eventData.target.transform.push( [ 'scale', windowScale, windowScale, 1 ] );
+ eventData.target.transform.reverse();
+ eventData.target.perspectiveScale /= windowScale;
+ }
+ eventData.current.zoomOriginWindowScale = windowScale;
+ } );
+ $.jmpress( 'setInactive', function( step, eventData ) {
+ if (
+ ! eventData.nextStep ||
+ ! step ||
+ $( eventData.nextStep ).attr( 'id' ) !== $( step ).attr( 'id' )
+ ) {
+ eventData.current.userZoom = 0;
+ eventData.current.userTranslateX = 0;
+ eventData.current.userTranslateY = 0;
+ }
+ } );
+} )( jQuery, document, window );
+
+/*
+ * mouse.js
+ * Clicking to select a step
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ var $jmpress = $.jmpress;
+
+ /* FUNCTIONS */
+ function randomString() {
+ return '' + Math.round( Math.random() * 100000, 0 );
+ }
+
+ /* DEFAULTS */
+ $jmpress( 'defaults' ).mouse = {
+ clickSelects: true,
+ };
+
+ /* HOOKS */
+ $jmpress( 'afterInit', function( nil, eventData ) {
+ var settings = eventData.settings,
+ stepSelector = settings.stepSelector,
+ current = eventData.current,
+ jmpress = $( this );
+ current.clickableStepsNamespace = '.jmpress-' + randomString();
+ jmpress.bind( 'click' + current.clickableStepsNamespace, function( event ) {
+ if ( ! settings.mouse.clickSelects || current.userZoom ) {
+ return;
+ }
+
+ // get clicked step
+ var clickedStep = $( event.target ).closest( stepSelector );
+
+ // clicks on the active step do default
+ if ( clickedStep.is( jmpress.jmpress( 'active' ) ) ) {
+ return;
+ }
+
+ if ( clickedStep.length ) {
+ // select the clicked step
+ jmpress.jmpress( 'select', clickedStep[ 0 ], 'click' );
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ } );
+ } );
+ $jmpress( 'afterDeinit', function( nil, eventData ) {
+ $( this ).unbind( eventData.current.clickableStepsNamespace );
+ } );
+} )( jQuery, document, window );
+/*
+ * mobile.js
+ * Adds support for swipe on touch supported browsers
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ var $jmpress = $.jmpress;
+
+ /* FUNCTIONS */
+ function randomString() {
+ return '' + Math.round( Math.random() * 100000, 0 );
+ }
+
+ /* HOOKS */
+ $jmpress( 'afterInit', function( step, eventData ) {
+ var settings = eventData.settings,
+ current = eventData.current,
+ jmpress = eventData.jmpress;
+ current.mobileNamespace = '.jmpress-' + randomString();
+ var data,
+ start = [ 0, 0 ];
+ $( settings.fullscreen ? document : jmpress )
+ .bind( 'touchstart' + current.mobileNamespace, function( event ) {
+ data = event.originalEvent.touches[ 0 ];
+ start = [ data.pageX, data.pageY ];
+ } )
+ .bind( 'touchmove' + current.mobileNamespace, function( event ) {
+ data = event.originalEvent.touches[ 0 ];
+ event.preventDefault();
+ return false;
+ } )
+ .bind( 'touchend' + current.mobileNamespace, function( event ) {
+ var end = [ data.pageX, data.pageY ],
+ diff = [ end[ 0 ] - start[ 0 ], end[ 1 ] - start[ 1 ] ];
+
+ if ( Math.max( Math.abs( diff[ 0 ] ), Math.abs( diff[ 1 ] ) ) > 50 ) {
+ diff = Math.abs( diff[ 0 ] ) > Math.abs( diff[ 1 ] ) ? diff[ 0 ] : diff[ 1 ];
+ $( jmpress ).jmpress( diff > 0 ? 'prev' : 'next' );
+ event.preventDefault();
+ return false;
+ }
+ } );
+ } );
+ $jmpress( 'afterDeinit', function( nil, eventData ) {
+ var settings = eventData.settings,
+ current = eventData.current,
+ jmpress = eventData.jmpress;
+ $( settings.fullscreen ? document : jmpress ).unbind( current.mobileNamespace );
+ } );
+} )( jQuery, document, window );
+/*
+ * templates.js
+ * The amazing template engine
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ var $jmpress = $.jmpress,
+ templateFromParentIdent = '_template_',
+ templateFromApplyIdent = '_applied_template_';
+
+ /* STATIC VARS */
+ var templates = {};
+
+ /* FUNCTIONS */
+ function addUndefined( target, values, prefix ) {
+ for ( var name in values ) {
+ var targetName = name;
+ if ( prefix ) {
+ targetName = prefix + targetName.substr( 0, 1 ).toUpperCase() + targetName.substr( 1 );
+ }
+ if ( $.isPlainObject( values[ name ] ) ) {
+ addUndefined( target, values[ name ], targetName );
+ } else if ( target[ targetName ] === undefined ) {
+ target[ targetName ] = values[ name ];
+ }
+ }
+ }
+ function applyChildrenTemplates( children, templateChildren ) {
+ if ( $.isArray( templateChildren ) ) {
+ if ( templateChildren.length < children.length ) {
+ $.error( 'more nested steps than children in template' );
+ } else {
+ children.each( function( idx, child ) {
+ child = $( child );
+ var tmpl = child.data( templateFromParentIdent ) || {};
+ addUndefined( tmpl, templateChildren[ idx ] );
+ child.data( templateFromParentIdent, tmpl );
+ } );
+ }
+ } else if ( $.isFunction( templateChildren ) ) {
+ children.each( function( idx, child ) {
+ child = $( child );
+ var tmpl = child.data( templateFromParentIdent ) || {};
+ addUndefined( tmpl, templateChildren( idx, child, children ) );
+ child.data( templateFromParentIdent, tmpl );
+ } );
+ } // TODO: else if(object)
+ }
+ function applyTemplate( data, element, template, eventData ) {
+ if ( template.children ) {
+ var children = element.children( eventData.settings.stepSelector );
+ applyChildrenTemplates( children, template.children );
+ }
+ applyTemplateData( data, template );
+ }
+ function applyTemplateData( data, template ) {
+ addUndefined( data, template );
+ }
+
+ /* HOOKS */
+ $jmpress( 'beforeInitStep', function( step, eventData ) {
+ step = $( step );
+ var data = eventData.data,
+ templateFromAttr = data.template,
+ templateFromApply = step.data( templateFromApplyIdent ),
+ templateFromParent = step.data( templateFromParentIdent );
+ if ( templateFromAttr ) {
+ $.each( templateFromAttr.split( ' ' ), function( idx, tmpl ) {
+ var template = templates[ tmpl ];
+ applyTemplate( data, step, template, eventData );
+ } );
+ }
+ if ( templateFromApply ) {
+ applyTemplate( data, step, templateFromApply, eventData );
+ }
+ if ( templateFromParent ) {
+ applyTemplate( data, step, templateFromParent, eventData );
+ step.data( templateFromParentIdent, null );
+ if ( templateFromParent.template ) {
+ $.each( templateFromParent.template.split( ' ' ), function( idx, tmpl ) {
+ var template = templates[ tmpl ];
+ applyTemplate( data, step, template, eventData );
+ } );
+ }
+ }
+ } );
+ $jmpress( 'beforeInit', function( nil, eventData ) {
+ var data = $jmpress( 'dataset', this ),
+ dataTemplate = data.template,
+ stepSelector = eventData.settings.stepSelector;
+ if ( dataTemplate ) {
+ var template = templates[ dataTemplate ];
+ applyChildrenTemplates(
+ $( this )
+ .find( stepSelector )
+ .filter( function() {
+ return ! $( this )
+ .parent()
+ .is( stepSelector );
+ } ),
+ template.children
+ );
+ }
+ } );
+
+ /* EXPORTED FUNCTIONS */
+ $jmpress( 'register', 'template', function( name, tmpl ) {
+ if ( templates[ name ] ) {
+ templates[ name ] = $.extend( true, {}, templates[ name ], tmpl );
+ } else {
+ templates[ name ] = $.extend( true, {}, tmpl );
+ }
+ } );
+ $jmpress( 'register', 'apply', function( selector, tmpl ) {
+ if ( ! tmpl ) {
+ // TODO ERROR because settings not found
+ var stepSelector = $( this ).jmpress( 'settings' ).stepSelector;
+ applyChildrenTemplates(
+ $( this )
+ .find( stepSelector )
+ .filter( function() {
+ return ! $( this )
+ .parent()
+ .is( stepSelector );
+ } ),
+ selector
+ );
+ } else if ( $.isArray( tmpl ) ) {
+ applyChildrenTemplates( $( selector ), tmpl );
+ } else {
+ var template;
+ if ( typeof tmpl === 'string' ) {
+ template = templates[ tmpl ];
+ } else {
+ template = $.extend( true, {}, tmpl );
+ }
+ $( selector ).each( function( idx, element ) {
+ element = $( element );
+ var tmpl = element.data( templateFromApplyIdent ) || {};
+ addUndefined( tmpl, template );
+ element.data( templateFromApplyIdent, tmpl );
+ } );
+ }
+ } );
+} )( jQuery, document, window );
+/*
+ * jqevents.js
+ * Fires jQuery events
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+
+ /* HOOKS */
+ // the events should not bubble up the tree
+ // elsewise nested jmpress would cause buggy behavior
+ $.jmpress( 'setActive', function( step, eventData ) {
+ if ( eventData.prevStep !== step ) {
+ $( step ).triggerHandler( 'enterStep' );
+ }
+ } );
+ $.jmpress( 'setInactive', function( step, eventData ) {
+ if ( eventData.nextStep !== step ) {
+ $( step ).triggerHandler( 'leaveStep' );
+ }
+ } );
+} )( jQuery, document, window );
+/*
+ * animation.js
+ * Apply custom animations to steps
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+
+ function parseSubstepInfo( str ) {
+ var arr = str.split( ' ' );
+ var className = arr[ 0 ];
+ var config = {
+ willClass: 'will-' + className,
+ doClass: 'do-' + className,
+ hasClass: 'has-' + className,
+ };
+ var state = '';
+ for ( var i = 1; i < arr.length; i++ ) {
+ var s = arr[ i ];
+ switch ( state ) {
+ case '':
+ if ( s === 'after' ) {
+ state = 'after';
+ } else {
+ $.warn( "unknown keyword in '" + str + "'. '" + s + "' unknown." );
+ }
+ break;
+ case 'after':
+ if ( s.match( /^[1-9][0-9]*m?s?/ ) ) {
+ var value = parseFloat( s );
+ if ( s.indexOf( 'ms' ) !== -1 ) {
+ value *= 1;
+ } else if ( s.indexOf( 's' ) !== -1 ) {
+ value *= 1000;
+ } else if ( s.indexOf( 'm' ) !== -1 ) {
+ value *= 60000;
+ }
+ config.delay = value;
+ } else {
+ config.after = Array.prototype.slice.call( arr, i ).join( ' ' );
+ i = arr.length;
+ }
+ }
+ }
+ return config;
+ }
+ function find( array, selector, start, end ) {
+ end = end || array.length - 1;
+ start = start || 0;
+ for ( var i = start; i < end + 1; i++ ) {
+ if ( $( array[ i ].element ).is( selector ) ) {
+ return i;
+ }
+ }
+ }
+ function addOn( list, substep, delay ) {
+ $.each( substep._on, function( idx, child ) {
+ list.push( { substep: child.substep, delay: child.delay + delay } );
+ addOn( list, child.substep, child.delay + delay );
+ } );
+ }
+ $.jmpress( 'defaults' ).customAnimationDataAttribute = 'jmpress';
+ $.jmpress( 'afterInit', function( nil, eventData ) {
+ eventData.current.animationTimeouts = [];
+ eventData.current.animationCleanupWaiting = [];
+ } );
+ $.jmpress( 'applyStep', function( step, eventData ) {
+ // read custom animation from elements
+ var substepsData = {};
+ var listOfSubsteps = [];
+ $( step )
+ .find( '[data-' + eventData.settings.customAnimationDataAttribute + ']' )
+ .each( function( idx, element ) {
+ if (
+ $( element )
+ .closest( eventData.settings.stepSelector )
+ .is( step )
+ ) {
+ listOfSubsteps.push( { element: element } );
+ }
+ } );
+ if ( listOfSubsteps.length === 0 ) {
+ return;
+ }
+ $.each( listOfSubsteps, function( idx, substep ) {
+ substep.info = parseSubstepInfo(
+ $( substep.element ).data( eventData.settings.customAnimationDataAttribute )
+ );
+ $( substep.element ).addClass( substep.info.willClass );
+ substep._on = [];
+ substep._after = null;
+ } );
+ var current = { _after: undefined, _on: [], info: {} }; // virtual zero step
+ $.each( listOfSubsteps, function( idx, substep ) {
+ var other = substep.info.after;
+ if ( other ) {
+ if ( other === 'step' ) {
+ other = current;
+ } else if ( other === 'prev' ) {
+ other = listOfSubsteps[ idx - 1 ];
+ } else {
+ var index = find( listOfSubsteps, other, 0, idx - 1 );
+ if ( index === undefined ) {
+ index = find( listOfSubsteps, other );
+ }
+ other =
+ index === undefined || index === idx
+ ? listOfSubsteps[ idx - 1 ]
+ : listOfSubsteps[ index ];
+ }
+ } else {
+ other = listOfSubsteps[ idx - 1 ];
+ }
+ if ( other ) {
+ if ( ! substep.info.delay ) {
+ if ( ! other._after ) {
+ other._after = substep;
+ return;
+ }
+ other = other._after;
+ }
+ other._on.push( { substep: substep, delay: substep.info.delay || 0 } );
+ }
+ } );
+ if ( current._after === undefined && current._on.length === 0 ) {
+ var startStep = find( listOfSubsteps, eventData.stepData.startSubstep ) || 0;
+ current._after = listOfSubsteps[ startStep ];
+ }
+ var substepsInOrder = [];
+ function findNextFunc( idx, item ) {
+ if ( item.substep._after ) {
+ current = item.substep._after;
+ return false;
+ }
+ }
+ do {
+ var substepList = [ { substep: current, delay: 0 } ];
+ addOn( substepList, current, 0 );
+ substepsInOrder.push( substepList );
+ current = null;
+ $.each( substepList, findNextFunc );
+ } while ( current );
+ substepsData.list = substepsInOrder;
+ $( step ).data( 'substepsData', substepsData );
+ } );
+ $.jmpress( 'unapplyStep', function( step, eventData ) {
+ var substepsData = $( step ).data( 'substepsData' );
+ if ( substepsData ) {
+ $.each( substepsData.list, function( idx, activeSubsteps ) {
+ $.each( activeSubsteps, function( idx, substep ) {
+ if ( substep.substep.info.willClass ) {
+ $( substep.substep.element ).removeClass( substep.substep.info.willClass );
+ }
+ if ( substep.substep.info.hasClass ) {
+ $( substep.substep.element ).removeClass( substep.substep.info.hasClass );
+ }
+ if ( substep.substep.info.doClass ) {
+ $( substep.substep.element ).removeClass( substep.substep.info.doClass );
+ }
+ } );
+ } );
+ }
+ } );
+ $.jmpress( 'setActive', function( step, eventData ) {
+ var substepsData = $( step ).data( 'substepsData' );
+ if ( ! substepsData ) {
+ return;
+ }
+ if ( eventData.substep === undefined ) {
+ eventData.substep = eventData.reason === 'prev' ? substepsData.list.length - 1 : 0;
+ }
+ var substep = eventData.substep;
+ $.each( eventData.current.animationTimeouts, function( idx, timeout ) {
+ clearTimeout( timeout );
+ } );
+ eventData.current.animationTimeouts = [];
+ $.each( substepsData.list, function( idx, activeSubsteps ) {
+ var applyHas = idx < substep;
+ var applyDo = idx <= substep;
+ $.each( activeSubsteps, function( idx, substep ) {
+ if ( substep.substep.info.hasClass ) {
+ $( substep.substep.element )[ ( applyHas ? 'add' : 'remove' ) + 'Class' ](
+ substep.substep.info.hasClass
+ );
+ }
+ function applyIt() {
+ $( substep.substep.element ).addClass( substep.substep.info.doClass );
+ }
+ if ( applyDo && ! applyHas && substep.delay && eventData.reason !== 'prev' ) {
+ if ( substep.substep.info.doClass ) {
+ $( substep.substep.element ).removeClass( substep.substep.info.doClass );
+ eventData.current.animationTimeouts.push( setTimeout( applyIt, substep.delay ) );
+ }
+ } else {
+ if ( substep.substep.info.doClass ) {
+ $( substep.substep.element )[ ( applyDo ? 'add' : 'remove' ) + 'Class' ](
+ substep.substep.info.doClass
+ );
+ }
+ }
+ } );
+ } );
+ } );
+ $.jmpress( 'setInactive', function( step, eventData ) {
+ if ( eventData.nextStep === step ) {
+ return;
+ }
+ function cleanupAnimation( substepsData ) {
+ $.each( substepsData.list, function( idx, activeSubsteps ) {
+ $.each( activeSubsteps, function( idx, substep ) {
+ if ( substep.substep.info.hasClass ) {
+ $( substep.substep.element ).removeClass( substep.substep.info.hasClass );
+ }
+ if ( substep.substep.info.doClass ) {
+ $( substep.substep.element ).removeClass( substep.substep.info.doClass );
+ }
+ } );
+ } );
+ }
+ $.each( eventData.current.animationCleanupWaiting, function( idx, item ) {
+ cleanupAnimation( item );
+ } );
+ eventData.current.animationCleanupWaiting = [];
+ var substepsData = $( step ).data( 'substepsData' );
+ if ( substepsData ) {
+ eventData.current.animationCleanupWaiting.push( substepsData );
+ }
+ } );
+ $.jmpress( 'selectNext', function( step, eventData ) {
+ if ( eventData.substep === undefined ) {
+ return;
+ }
+ var substepsData = $( step ).data( 'substepsData' );
+ if ( ! substepsData ) {
+ return;
+ }
+ if ( eventData.substep < substepsData.list.length - 1 ) {
+ return { step: step, substep: eventData.substep + 1 };
+ }
+ } );
+ $.jmpress( 'selectPrev', function( step, eventData ) {
+ if ( eventData.substep === undefined ) {
+ return;
+ }
+ var substepsData = $( step ).data( 'substepsData' );
+ if ( ! substepsData ) {
+ return;
+ }
+ if ( eventData.substep > 0 ) {
+ return { step: step, substep: eventData.substep - 1 };
+ }
+ } );
+} )( jQuery, document, window );
+/*
+ * jmpress.toggle plugin
+ * For binding a key to toggle de/initialization of jmpress.js.
+ */
+/*!
+ * plugin for jmpress.js v0.4.5
+ *
+ * Copyright 2013 Kyle Robinson Young @shama & Tobias Koppers @sokra
+ * Licensed MIT
+ * http://www.opensource.org/licenses/mit-license.php
+ */ ( function( $, document, window, undefined ) {
+ 'use strict';
+ $.jmpress( 'register', 'toggle', function( key, config, initial ) {
+ var jmpress = this;
+ $( document ).bind( 'keydown', function( event ) {
+ if ( event.keyCode === key ) {
+ if ( $( jmpress ).jmpress( 'initialized' ) ) {
+ $( jmpress ).jmpress( 'deinit' );
+ } else {
+ $( jmpress ).jmpress( config );
+ }
+ }
+ } );
+ if ( initial ) {
+ $( jmpress ).jmpress( config );
+ }
+ } );
+} )( jQuery, document, window );
+
+/*
+ * jmpress.secondary plugin
+ * Apply a secondary animation when step is selected.
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ $.jmpress( 'initStep', function( step, eventData ) {
+ for ( var name in eventData.data ) {
+ if ( name.indexOf( 'secondary' ) === 0 ) {
+ eventData.stepData[ name ] = eventData.data[ name ];
+ }
+ }
+ } );
+ function exchangeIf( childStepData, condition, step ) {
+ if (
+ childStepData.secondary &&
+ childStepData.secondary.split( ' ' ).indexOf( condition ) !== -1
+ ) {
+ for ( var name in childStepData ) {
+ if ( name.length > 9 && name.indexOf( 'secondary' ) === 0 ) {
+ var tmp = childStepData[ name ];
+ var normal = name.substr( 9 );
+ normal = normal.substr( 0, 1 ).toLowerCase() + normal.substr( 1 );
+ childStepData[ name ] = childStepData[ normal ];
+ childStepData[ normal ] = tmp;
+ }
+ }
+ $( this ).jmpress( 'reapply', $( step ) );
+ }
+ }
+ $.jmpress( 'beforeActive', function( step, eventData ) {
+ exchangeIf.call( eventData.jmpress, $( step ).data( 'stepData' ), 'self', step );
+ var parent = $( step ).parent();
+ $( parent )
+ .children( eventData.settings.stepSelector )
+ .each( function( idx, child ) {
+ var childStepData = $( child ).data( 'stepData' );
+ exchangeIf.call( eventData.jmpress, childStepData, 'siblings', child );
+ } );
+ function grandchildrenFunc( idx, child ) {
+ var childStepData = $( child ).data( 'stepData' );
+ exchangeIf.call( eventData.jmpress, childStepData, 'grandchildren', child );
+ }
+ for ( var i = 1; i < eventData.parents.length; i++ ) {
+ $( eventData.parents[ i ] )
+ .children( eventData.settings.stepSelector )
+ .each();
+ }
+ } );
+ $.jmpress( 'setInactive', function( step, eventData ) {
+ exchangeIf.call( eventData.jmpress, $( step ).data( 'stepData' ), 'self', step );
+ var parent = $( step ).parent();
+ $( parent )
+ .children( eventData.settings.stepSelector )
+ .each( function( idx, child ) {
+ var childStepData = $( child ).data( 'stepData' );
+ exchangeIf.call( eventData.jmpress, childStepData, 'siblings', child );
+ } );
+ function grandchildrenFunc( idx, child ) {
+ var childStepData = $( child ).data( 'stepData' );
+ exchangeIf.call( eventData.jmpress, childStepData, 'grandchildren', child );
+ }
+ for ( var i = 1; i < eventData.parents.length; i++ ) {
+ $( eventData.parents[ i ] )
+ .children( eventData.settings.stepSelector )
+ .each( grandchildrenFunc );
+ }
+ } );
+} )( jQuery, document, window );
+
+/*
+ * jmpress.duration plugin
+ * For auto advancing steps after a given duration and optionally displaying a
+ * progress bar.
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+
+ $.jmpress( 'defaults' ).duration = {
+ defaultValue: -1,
+ defaultAction: 'next',
+ barSelector: undefined,
+ barProperty: 'width',
+ barPropertyStart: '0',
+ barPropertyEnd: '100%',
+ };
+ $.jmpress( 'initStep', function( step, eventData ) {
+ eventData.stepData.duration =
+ eventData.data.duration && parseInt( eventData.data.duration, 10 );
+ eventData.stepData.durationAction = eventData.data.durationAction;
+ } );
+ $.jmpress( 'setInactive', function( step, eventData ) {
+ var settings = eventData.settings,
+ durationSettings = settings.duration,
+ current = eventData.current;
+ var dur = eventData.stepData.duration || durationSettings.defaultValue;
+ if ( current.durationTimeout ) {
+ if ( durationSettings.barSelector ) {
+ var css = {
+ transitionProperty: durationSettings.barProperty,
+ transitionDuration: '0',
+ transitionDelay: '0',
+ transitionTimingFunction: 'linear',
+ };
+ css[ durationSettings.barProperty ] = durationSettings.barPropertyStart;
+ var bars = $( durationSettings.barSelector );
+ $.jmpress( 'css', bars, css );
+ bars.each( function( idx, element ) {
+ var next = $( element ).next();
+ var parent = $( element ).parent();
+ $( element ).detach();
+ if ( next.length ) {
+ next.insertBefore( element );
+ } else {
+ parent.append( element );
+ }
+ } );
+ }
+ clearTimeout( current.durationTimeout );
+ delete current.durationTimeout;
+ }
+ } );
+ $.jmpress( 'setActive', function( step, eventData ) {
+ var settings = eventData.settings,
+ durationSettings = settings.duration,
+ current = eventData.current;
+ var dur = eventData.stepData.duration || durationSettings.defaultValue;
+ if ( dur && dur > 0 ) {
+ if ( durationSettings.barSelector ) {
+ var css = {
+ transitionProperty: durationSettings.barProperty,
+ transitionDuration: dur - ( settings.transitionDuration * 2 ) / 3 - 100 + 'ms',
+ transitionDelay: ( settings.transitionDuration * 2 ) / 3 + 'ms',
+ transitionTimingFunction: 'linear',
+ };
+ css[ durationSettings.barProperty ] = durationSettings.barPropertyEnd;
+ $.jmpress( 'css', $( durationSettings.barSelector ), css );
+ }
+ var jmpress = this;
+ if ( current.durationTimeout ) {
+ clearTimeout( current.durationTimeout );
+ current.durationTimeout = undefined;
+ }
+ current.durationTimeout = setTimeout( function() {
+ var action = eventData.stepData.durationAction || durationSettings.defaultAction;
+ $( jmpress ).jmpress( action );
+ }, dur );
+ }
+ } );
+} )( jQuery, document, window );
+
+/*
+ * jmpress.presentation-mode plugin
+ * Display a window for the presenter with notes and a control and view of the
+ * presentation
+ */
+( function( $, document, window, undefined ) {
+ 'use strict';
+ var $jmpress = $.jmpress;
+
+ var PREFIX = 'jmpress-presentation-';
+
+ /* FUNCTIONS */
+ function randomString() {
+ return '' + Math.round( Math.random() * 100000, 0 );
+ }
+
+ /* DEFAULTS */
+ $jmpress( 'defaults' ).presentationMode = {
+ use: true,
+ url: 'presentation-screen.html',
+ notesUrl: false,
+ transferredValues: [ 'userZoom', 'userTranslateX', 'userTranslateY' ],
+ };
+ $jmpress( 'defaults' ).keyboard.keys[ 80 ] = 'presentationPopup'; // p key
+
+ /* HOOKS */
+ $jmpress( 'afterInit', function( nil, eventData ) {
+ var current = eventData.current;
+
+ current.selectMessageListeners = [];
+
+ if ( eventData.settings.presentationMode.use ) {
+ window.addEventListener( 'message', function( event ) {
+ // We do not test orgin, because we want to accept messages
+ // from all orgins
+ try {
+ if ( typeof event.data !== 'string' || event.data.indexOf( PREFIX ) !== 0 ) {
+ return;
+ }
+ var json = JSON.parse( event.data.slice( PREFIX.length ) );
+ switch ( json.type ) {
+ case 'select':
+ $.each( eventData.settings.presentationMode.transferredValues, function( idx, name ) {
+ eventData.current[ name ] = json[ name ];
+ } );
+ if (
+ /[a-z0-9\-]+/i.test( json.targetId ) &&
+ typeof json.substep in { number: 1, undefined: 1 }
+ ) {
+ $( eventData.jmpress ).jmpress(
+ 'select',
+ { step: '#' + json.targetId, substep: json.substep },
+ json.reason
+ );
+ } else {
+ $.error(
+ 'For security reasons the targetId must match /[a-z0-9\\-]+/i and substep must be a number.'
+ );
+ }
+ break;
+ case 'listen':
+ current.selectMessageListeners.push( event.source );
+ break;
+ case 'ok':
+ clearTimeout( current.presentationPopupTimeout );
+ break;
+ case 'read':
+ try {
+ event.source.postMessage(
+ PREFIX +
+ JSON.stringify( {
+ type: 'url',
+ url: window.location.href,
+ notesUrl: eventData.settings.presentationMode.notesUrl,
+ } ),
+ '*'
+ );
+ } catch ( e ) {
+ $.error( 'Cannot post message to source: ' + e );
+ }
+ break;
+ default:
+ throw 'Unknown message type: ' + json.type;
+ }
+ } catch ( e ) {
+ $.error( 'Received message is malformed: ' + e );
+ }
+ } );
+ try {
+ if ( window.parent && window.parent !== window ) {
+ window.parent.postMessage(
+ PREFIX +
+ JSON.stringify( {
+ type: 'afterInit',
+ } ),
+ '*'
+ );
+ }
+ } catch ( e ) {
+ $.error( 'Cannot post message to parent: ' + e );
+ }
+ }
+ } );
+ $jmpress( 'afterDeinit', function( nil, eventData ) {
+ if ( eventData.settings.presentationMode.use ) {
+ try {
+ if ( window.parent && window.parent !== window ) {
+ window.parent.postMessage(
+ PREFIX +
+ JSON.stringify( {
+ type: 'afterDeinit',
+ } ),
+ '*'
+ );
+ }
+ } catch ( e ) {
+ $.error( 'Cannot post message to parent: ' + e );
+ }
+ }
+ } );
+ $jmpress( 'setActive', function( step, eventData ) {
+ var stepId = $( eventData.delegatedFrom ).attr( 'id' ),
+ substep = eventData.substep,
+ reason = eventData.reason;
+ $.each( eventData.current.selectMessageListeners, function( idx, listener ) {
+ try {
+ var msg = {
+ type: 'select',
+ targetId: stepId,
+ substep: substep,
+ reason: reason,
+ };
+ $.each( eventData.settings.presentationMode.transferredValues, function( idx, name ) {
+ msg[ name ] = eventData.current[ name ];
+ } );
+ listener.postMessage( PREFIX + JSON.stringify( msg ), '*' );
+ } catch ( e ) {
+ $.error( 'Cannot post message to listener: ' + e );
+ }
+ } );
+ } );
+ $jmpress( 'register', 'presentationPopup', function() {
+ function trySend() {
+ jmpress.jmpress( 'current' ).presentationPopupTimeout = setTimeout( trySend, 100 );
+ try {
+ popup.postMessage(
+ PREFIX +
+ JSON.stringify( {
+ type: 'url',
+ url: window.location.href,
+ notesUrl: jmpress.jmpress( 'settings' ).presentationMode.notesUrl,
+ } ),
+ '*'
+ );
+ } catch ( e ) {}
+ }
+ var jmpress = $( this ),
+ popup;
+ if ( jmpress.jmpress( 'settings' ).presentationMode.use ) {
+ popup = window.open( $( this ).jmpress( 'settings' ).presentationMode.url );
+ jmpress.jmpress( 'current' ).presentationPopupTimeout = setTimeout( trySend, 100 );
+ }
+ } );
+} )( jQuery, document, window );
diff --git a/plugins/jetpack/modules/shortcodes/js/jquery.cycle.min.js b/plugins/jetpack/modules/shortcodes/js/jquery.cycle.min.js
new file mode 100644
index 00000000..35a4d0cf
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/js/jquery.cycle.min.js
@@ -0,0 +1,9 @@
+/*!
+ * jQuery Cycle Plugin (with Transition Definitions)
+ * Examples and documentation at: http://jquery.malsup.com/cycle/
+ * Copyright (c) 2007-2010 M. Alsup
+ * Version: 2.9999.8 (26-OCT-2012)
+ * Dual licensed under the MIT and GPL licenses.
+ * http://jquery.malsup.com/license.html
+ */
+!function(e,t){"use strict";function n(t){e.fn.cycle.debug&&i(t)}function i(){window.console&&console.log&&console.log("[cycle] "+Array.prototype.join.call(arguments," "))}function c(t,n,i){var c=e(t).data("cycle.opts");if(c){var s=!!t.cyclePause;s&&c.paused?c.paused(t,c,n,i):!s&&c.resumed&&c.resumed(t,c,n,i)}}function s(n,s,o){function l(t,n,c){if(!t&&n===!0){var s=e(c).data("cycle.opts");if(!s)return i("options not found, can not resume"),!1;c.cycleTimeout&&(clearTimeout(c.cycleTimeout),c.cycleTimeout=0),d(s.elements,s,1,!s.backwards)}}if(n.cycleStop===t&&(n.cycleStop=0),s!==t&&null!==s||(s={}),s.constructor==String){switch(s){case"destroy":case"stop":var a=e(n).data("cycle.opts");return a?(n.cycleStop++,n.cycleTimeout&&clearTimeout(n.cycleTimeout),n.cycleTimeout=0,a.elements&&e(a.elements).stop(),e(n).removeData("cycle.opts"),"destroy"==s&&r(n,a),!1):!1;case"toggle":return n.cyclePause=1===n.cyclePause?0:1,l(n.cyclePause,o,n),c(n),!1;case"pause":return n.cyclePause=1,c(n),!1;case"resume":return n.cyclePause=0,l(!1,o,n),c(n),!1;case"prev":case"next":return(a=e(n).data("cycle.opts"))?(e.fn.cycle[s](a),!1):(i('options not found, "prev/next" ignored'),!1);default:s={fx:s}}return s}if(s.constructor==Number){var f=s;return(s=e(n).data("cycle.opts"))?0>f||f>=s.elements.length?(i("invalid slide index: "+f),!1):(s.nextSlide=f,n.cycleTimeout&&(clearTimeout(n.cycleTimeout),n.cycleTimeout=0),"string"==typeof o&&(s.oneTimeFx=o),d(s.elements,s,1,f>=s.currSlide),!1):(i("options not found, can not advance slide"),!1)}return s}function o(t,n){if(!e.support.opacity&&n.cleartype&&t.style.filter)try{t.style.removeAttribute("filter")}catch(i){}}function r(t,n){n.next&&e(n.next).unbind(n.prevNextEvent),n.prev&&e(n.prev).unbind(n.prevNextEvent),(n.pager||n.pagerAnchorBuilder)&&e.each(n.pagerAnchors||[],function(){this.unbind().remove()}),n.pagerAnchors=null,e(t).unbind("mouseenter.cycle mouseleave.cycle"),n.destroy&&n.destroy(n)}function l(n,s,r,l,h){var g,x=e.extend({},e.fn.cycle.defaults,l||{},e.metadata?n.metadata():e.meta?n.data():{}),v=e.isFunction(n.data)?n.data(x.metaAttr):null;v&&(x=e.extend(x,v)),x.autostop&&(x.countdown=x.autostopCount||r.length);var w=n[0];if(n.data("cycle.opts",x),x.$cont=n,x.stopCount=w.cycleStop,x.elements=r,x.before=x.before?[x.before]:[],x.after=x.after?[x.after]:[],!e.support.opacity&&x.cleartype&&x.after.push(function(){o(this,x)}),x.continuous&&x.after.push(function(){d(r,x,0,!x.backwards)}),a(x),e.support.opacity||!x.cleartype||x.cleartypeNoBg||y(s),"static"==n.css("position")&&n.css("position","relative"),x.width&&n.width(x.width),x.height&&"auto"!=x.height&&n.height(x.height),x.startingSlide!==t?(x.startingSlide=parseInt(x.startingSlide,10),x.startingSlide>=r.length||x.startSlide<0?x.startingSlide=0:g=!0):x.backwards?x.startingSlide=r.length-1:x.startingSlide=0,x.random){x.randomMap=[];for(var b=0;b<r.length;b++)x.randomMap.push(b);if(x.randomMap.sort(function(){return Math.random()-.5}),g)for(var S=0;S<r.length;S++)x.startingSlide==x.randomMap[S]&&(x.randomIndex=S);else x.randomIndex=1,x.startingSlide=x.randomMap[1]}else x.startingSlide>=r.length&&(x.startingSlide=0);x.currSlide=x.startingSlide||0;var B=x.startingSlide;s.css({position:"absolute",top:0,left:0}).hide().each(function(t){var n;n=x.backwards?B?B>=t?r.length+(t-B):B-t:r.length-t:B?t>=B?r.length-(t-B):B-t:r.length-t,e(this).css("z-index",n)}),e(r[B]).css("opacity",1).show(),o(r[B],x),x.fit&&(x.aspect?s.each(function(){var t=e(this),n=x.aspect===!0?t.width()/t.height():x.aspect;x.width&&t.width()!=x.width&&(t.width(x.width),t.height(x.width/n)),x.height&&t.height()<x.height&&(t.height(x.height),t.width(x.height*n))}):(x.width&&s.width(x.width),x.height&&"auto"!=x.height&&s.height(x.height))),!x.center||x.fit&&!x.aspect||s.each(function(){var t=e(this);t.css({"margin-left":x.width?(x.width-t.width())/2+"px":0,"margin-top":x.height?(x.height-t.height())/2+"px":0})}),!x.center||x.fit||x.slideResize||s.each(function(){var t=e(this);t.css({"margin-left":x.width?(x.width-t.width())/2+"px":0,"margin-top":x.height?(x.height-t.height())/2+"px":0})});var I=(x.containerResize||x.containerResizeHeight)&&!n.innerHeight();if(I){for(var O=0,F=0,A=0;A<r.length;A++){var H=e(r[A]),k=H[0],T=H.outerWidth(),R=H.outerHeight();T||(T=k.offsetWidth||k.width||H.attr("width")),R||(R=k.offsetHeight||k.height||H.attr("height")),O=T>O?T:O,F=R>F?R:F}x.containerResize&&O>0&&F>0&&n.css({width:O+"px",height:F+"px"}),x.containerResizeHeight&&F>0&&n.css({height:F+"px"})}var W=!1;if(x.pause&&n.bind("mouseenter.cycle",function(){W=!0,this.cyclePause++,c(w,!0)}).bind("mouseleave.cycle",function(){W&&this.cyclePause--,c(w,!0)}),f(x)===!1)return!1;var P=!1;if(l.requeueAttempts=l.requeueAttempts||0,s.each(function(){var t=e(this);if(this.cycleH=x.fit&&x.height?x.height:t.height()||this.offsetHeight||this.height||t.attr("height")||0,this.cycleW=x.fit&&x.width?x.width:t.width()||this.offsetWidth||this.width||t.attr("width")||0,t.is("img")){var n=e.browser.msie&&28==this.cycleW&&30==this.cycleH&&!this.complete,c=e.browser.mozilla&&34==this.cycleW&&19==this.cycleH&&!this.complete,s=e.browser.opera&&(42==this.cycleW&&19==this.cycleH||37==this.cycleW&&17==this.cycleH)&&!this.complete,o=0===this.cycleH&&0===this.cycleW&&!this.complete;if(n||c||s||o){if(h.s&&x.requeueOnImageNotLoaded&&++l.requeueAttempts<100)return i(l.requeueAttempts," - img slide not loaded, requeuing slideshow: ",this.src,this.cycleW,this.cycleH),setTimeout(function(){e(h.s,h.c).cycle(l)},x.requeueTimeout),P=!0,!1;i("could not determine size of image: "+this.src,this.cycleW,this.cycleH)}}return!0}),P)return!1;if(x.cssBefore=x.cssBefore||{},x.cssAfter=x.cssAfter||{},x.cssFirst=x.cssFirst||{},x.animIn=x.animIn||{},x.animOut=x.animOut||{},s.not(":eq("+B+")").css(x.cssBefore),e(s[B]).css(x.cssFirst),x.timeout){x.timeout=parseInt(x.timeout,10),x.speed.constructor==String&&(x.speed=e.fx.speeds[x.speed]||parseInt(x.speed,10)),x.sync||(x.speed=x.speed/2);for(var z="none"==x.fx?0:"shuffle"==x.fx?500:250;x.timeout-x.speed<z;)x.timeout+=x.speed}if(x.easing&&(x.easeIn=x.easeOut=x.easing),x.speedIn||(x.speedIn=x.speed),x.speedOut||(x.speedOut=x.speed),x.slideCount=r.length,x.currSlide=x.lastSlide=B,x.random?(++x.randomIndex==r.length&&(x.randomIndex=0),x.nextSlide=x.randomMap[x.randomIndex]):x.backwards?x.nextSlide=0===x.startingSlide?r.length-1:x.startingSlide-1:x.nextSlide=x.startingSlide>=r.length-1?0:x.startingSlide+1,!x.multiFx){var C=e.fn.cycle.transitions[x.fx];if(e.isFunction(C))C(n,s,x);else if("custom"!=x.fx&&!x.multiFx)return i("unknown transition: "+x.fx,"; slideshow terminating"),!1}var E=s[B];return x.skipInitializationCallbacks||(x.before.length&&x.before[0].apply(E,[E,E,x,!0]),x.after.length&&x.after[0].apply(E,[E,E,x,!0])),x.next&&e(x.next).bind(x.prevNextEvent,function(){return p(x,1)}),x.prev&&e(x.prev).bind(x.prevNextEvent,function(){return p(x,0)}),(x.pager||x.pagerAnchorBuilder)&&m(r,x),u(x,r),x}function a(t){t.original={before:[],after:[]},t.original.cssBefore=e.extend({},t.cssBefore),t.original.cssAfter=e.extend({},t.cssAfter),t.original.animIn=e.extend({},t.animIn),t.original.animOut=e.extend({},t.animOut),e.each(t.before,function(){t.original.before.push(this)}),e.each(t.after,function(){t.original.after.push(this)})}function f(t){var c,s,o=e.fn.cycle.transitions;if(t.fx.indexOf(",")>0){for(t.multiFx=!0,t.fxs=t.fx.replace(/\s*/g,"").split(","),c=0;c<t.fxs.length;c++){var r=t.fxs[c];s=o[r],s&&o.hasOwnProperty(r)&&e.isFunction(s)||(i("discarding unknown transition: ",r),t.fxs.splice(c,1),c--)}if(!t.fxs.length)return i("No valid transitions named; slideshow terminating."),!1}else if("all"==t.fx){t.multiFx=!0,t.fxs=[];for(var l in o)o.hasOwnProperty(l)&&(s=o[l],o.hasOwnProperty(l)&&e.isFunction(s)&&t.fxs.push(l))}if(t.multiFx&&t.randomizeEffects){var a=Math.floor(20*Math.random())+30;for(c=0;a>c;c++){var f=Math.floor(Math.random()*t.fxs.length);t.fxs.push(t.fxs.splice(f,1)[0])}n("randomized fx sequence: ",t.fxs)}return!0}function u(t,n){t.addSlide=function(i,c){var s=e(i),o=s[0];t.autostopCount||t.countdown++,n[c?"unshift":"push"](o),t.els&&t.els[c?"unshift":"push"](o),t.slideCount=n.length,t.random&&(t.randomMap.push(t.slideCount-1),t.randomMap.sort(function(){return Math.random()-.5})),s.css("position","absolute"),s[c?"prependTo":"appendTo"](t.$cont),c&&(t.currSlide++,t.nextSlide++),e.support.opacity||!t.cleartype||t.cleartypeNoBg||y(s),t.fit&&t.width&&s.width(t.width),t.fit&&t.height&&"auto"!=t.height&&s.height(t.height),o.cycleH=t.fit&&t.height?t.height:s.height(),o.cycleW=t.fit&&t.width?t.width:s.width(),s.css(t.cssBefore),(t.pager||t.pagerAnchorBuilder)&&e.fn.cycle.createPagerAnchor(n.length-1,o,e(t.pager),n,t),e.isFunction(t.onAddSlide)?t.onAddSlide(s):s.hide()}}function d(i,c,s,o){function r(){var e=0;c.timeout;c.timeout&&!c.continuous?(e=h(i[c.currSlide],i[c.nextSlide],c,o),"shuffle"==c.fx&&(e-=c.speedOut)):c.continuous&&l.cyclePause&&(e=10),e>0&&(l.cycleTimeout=setTimeout(function(){d(i,c,0,!c.backwards)},e))}var l=c.$cont[0],a=i[c.currSlide],f=i[c.nextSlide];if(s&&c.busy&&c.manualTrump&&(n("manualTrump in go(), stopping active transition"),e(i).stop(!0,!0),c.busy=0,clearTimeout(l.cycleTimeout)),c.busy)return void n("transition active, ignoring new tx request");if(l.cycleStop==c.stopCount&&(0!==l.cycleTimeout||s)){if(!s&&!l.cyclePause&&!c.bounce&&(c.autostop&&--c.countdown<=0||c.nowrap&&!c.random&&c.nextSlide<c.currSlide))return void(c.end&&c.end(c));var u=!1;if(!s&&l.cyclePause||c.nextSlide==c.currSlide)r();else{u=!0;var p=c.fx;a.cycleH=a.cycleH||e(a).height(),a.cycleW=a.cycleW||e(a).width(),f.cycleH=f.cycleH||e(f).height(),f.cycleW=f.cycleW||e(f).width(),c.multiFx&&(o&&(c.lastFx===t||++c.lastFx>=c.fxs.length)?c.lastFx=0:!o&&(c.lastFx===t||--c.lastFx<0)&&(c.lastFx=c.fxs.length-1),p=c.fxs[c.lastFx]),c.oneTimeFx&&(p=c.oneTimeFx,c.oneTimeFx=null),e.fn.cycle.resetState(c,p),c.before.length&&e.each(c.before,function(e,t){l.cycleStop==c.stopCount&&t.apply(f,[a,f,c,o])});var m=function(){c.busy=0,e.each(c.after,function(e,t){l.cycleStop==c.stopCount&&t.apply(f,[a,f,c,o])}),l.cycleStop||r()};n("tx firing("+p+"); currSlide: "+c.currSlide+"; nextSlide: "+c.nextSlide),c.busy=1,c.fxFn?c.fxFn(a,f,c,m,o,s&&c.fastOnEvent):e.isFunction(e.fn.cycle[c.fx])?e.fn.cycle[c.fx](a,f,c,m,o,s&&c.fastOnEvent):e.fn.cycle.custom(a,f,c,m,o,s&&c.fastOnEvent)}if(u||c.nextSlide==c.currSlide){var y;c.lastSlide=c.currSlide,c.random?(c.currSlide=c.nextSlide,++c.randomIndex==i.length&&(c.randomIndex=0,c.randomMap.sort(function(){return Math.random()-.5})),c.nextSlide=c.randomMap[c.randomIndex],c.nextSlide==c.currSlide&&(c.nextSlide=c.currSlide==c.slideCount-1?0:c.currSlide+1)):c.backwards?(y=c.nextSlide-1<0,y&&c.bounce?(c.backwards=!c.backwards,c.nextSlide=1,c.currSlide=0):(c.nextSlide=y?i.length-1:c.nextSlide-1,c.currSlide=y?0:c.nextSlide+1)):(y=c.nextSlide+1==i.length,y&&c.bounce?(c.backwards=!c.backwards,c.nextSlide=i.length-2,c.currSlide=i.length-1):(c.nextSlide=y?0:c.nextSlide+1,c.currSlide=y?i.length-1:c.nextSlide-1))}u&&c.pager&&c.updateActivePagerLink(c.pager,c.currSlide,c.activePagerClass)}}function h(e,t,i,c){if(i.timeoutFn){for(var s=i.timeoutFn.call(e,e,t,i,c);"none"!=i.fx&&s-i.speed<250;)s+=i.speed;if(n("calculated timeout: "+s+"; speed: "+i.speed),s!==!1)return s}return i.timeout}function p(t,n){var i=n?1:-1,c=t.elements,s=t.$cont[0],o=s.cycleTimeout;if(o&&(clearTimeout(o),s.cycleTimeout=0),t.random&&0>i)t.randomIndex--,-2==--t.randomIndex?t.randomIndex=c.length-2:-1==t.randomIndex&&(t.randomIndex=c.length-1),t.nextSlide=t.randomMap[t.randomIndex];else if(t.random)t.nextSlide=t.randomMap[t.randomIndex];else if(t.nextSlide=t.currSlide+i,t.nextSlide<0){if(t.nowrap)return!1;t.nextSlide=c.length-1}else if(t.nextSlide>=c.length){if(t.nowrap)return!1;t.nextSlide=0}var r=t.onPrevNextEvent||t.prevNextClick;return e.isFunction(r)&&r(i>0,t.nextSlide,c[t.nextSlide]),d(c,t,1,n),!1}function m(t,n){var i=e(n.pager);e.each(t,function(c,s){e.fn.cycle.createPagerAnchor(c,s,i,t,n)}),n.updateActivePagerLink(n.pager,n.startingSlide,n.activePagerClass)}function y(t){function i(e){return e=parseInt(e,10).toString(16),e.length<2?"0"+e:e}function c(t){for(;t&&"html"!=t.nodeName.toLowerCase();t=t.parentNode){var n=e.css(t,"background-color");if(n&&n.indexOf("rgb")>=0){var c=n.match(/\d+/g);return"#"+i(c[0])+i(c[1])+i(c[2])}if(n&&"transparent"!=n)return n}return"#ffffff"}n("applying clearType background-color hack"),t.each(function(){e(this).css("background-color",c(this))})}var g="2.9999.8";e.support===t&&(e.support={opacity:!e.browser.msie}),e.expr[":"].paused=function(e){return e.cyclePause},e.fn.cycle=function(t,c){var o={s:this.selector,c:this.context};return 0===this.length&&"stop"!=t?!e.isReady&&o.s?(i("DOM not ready, queuing slideshow"),e(function(){e(o.s,o.c).cycle(t,c)}),this):(i("terminating; zero elements found by selector"+(e.isReady?"":" (DOM not ready)")),this):this.each(function(){var r=s(this,t,c);if(r!==!1){r.updateActivePagerLink=r.updateActivePagerLink||e.fn.cycle.updateActivePagerLink,this.cycleTimeout&&clearTimeout(this.cycleTimeout),this.cycleTimeout=this.cyclePause=0,this.cycleStop=0;var a=e(this),f=r.slideExpr?e(r.slideExpr,this):a.children(),u=f.get();if(u.length<2)return void i("terminating; too few slides: "+u.length);var p=l(a,f,u,r,o);if(p!==!1){var m=p.continuous?10:h(u[p.currSlide],u[p.nextSlide],p,!p.backwards);m&&(m+=p.delay||0,10>m&&(m=10),n("first timeout: "+m),this.cycleTimeout=setTimeout(function(){d(u,p,0,!r.backwards)},m))}}})},e.fn.cycle.resetState=function(t,n){n=n||t.fx,t.before=[],t.after=[],t.cssBefore=e.extend({},t.original.cssBefore),t.cssAfter=e.extend({},t.original.cssAfter),t.animIn=e.extend({},t.original.animIn),t.animOut=e.extend({},t.original.animOut),t.fxFn=null,e.each(t.original.before,function(){t.before.push(this)}),e.each(t.original.after,function(){t.after.push(this)});var i=e.fn.cycle.transitions[n];e.isFunction(i)&&i(t.$cont,e(t.elements),t)},e.fn.cycle.updateActivePagerLink=function(t,n,i){e(t).each(function(){e(this).children().removeClass(i).eq(n).addClass(i)})},e.fn.cycle.next=function(e){p(e,1)},e.fn.cycle.prev=function(e){p(e,0)},e.fn.cycle.createPagerAnchor=function(t,i,s,o,r){var l;if(e.isFunction(r.pagerAnchorBuilder)?(l=r.pagerAnchorBuilder(t,i),n("pagerAnchorBuilder("+t+", el) returned: "+l)):l='<a href="#">'+(t+1)+"</a>",l){var a=e(l);if(0===a.parents("body").length){var f=[];s.length>1?(s.each(function(){var t=a.clone(!0);e(this).append(t),f.push(t[0])}),a=e(f)):a.appendTo(s)}r.pagerAnchors=r.pagerAnchors||[],r.pagerAnchors.push(a);var u=function(n){n.preventDefault(),r.nextSlide=t;var i=r.$cont[0],c=i.cycleTimeout;c&&(clearTimeout(c),i.cycleTimeout=0);var s=r.onPagerEvent||r.pagerClick;e.isFunction(s)&&s(r.nextSlide,o[r.nextSlide]),d(o,r,1,r.currSlide<t)};/mouseenter|mouseover/i.test(r.pagerEvent)?a.hover(u,function(){}):a.bind(r.pagerEvent,u),/^click/.test(r.pagerEvent)||r.allowPagerClickBubble||a.bind("click.cycle",function(){return!1});var h=r.$cont[0],p=!1;r.pauseOnPagerHover&&a.hover(function(){p=!0,h.cyclePause++,c(h,!0,!0)},function(){p&&h.cyclePause--,c(h,!0,!0)})}},e.fn.cycle.hopsFromLast=function(e,t){var n,i=e.lastSlide,c=e.currSlide;return n=t?c>i?c-i:e.slideCount-i:i>c?i-c:i+e.slideCount-c},e.fn.cycle.commonReset=function(t,n,i,c,s,o){e(i.elements).not(t).hide(),"undefined"==typeof i.cssBefore.opacity&&(i.cssBefore.opacity=1),i.cssBefore.display="block",i.slideResize&&c!==!1&&n.cycleW>0&&(i.cssBefore.width=n.cycleW),i.slideResize&&s!==!1&&n.cycleH>0&&(i.cssBefore.height=n.cycleH),i.cssAfter=i.cssAfter||{},i.cssAfter.display="none",e(t).css("zIndex",i.slideCount+(o===!0?1:0)),e(n).css("zIndex",i.slideCount+(o===!0?0:1))},e.fn.cycle.custom=function(t,n,i,c,s,o){var r=e(t),l=e(n),a=i.speedIn,f=i.speedOut,u=i.easeIn,d=i.easeOut;l.css(i.cssBefore),o&&(a=f="number"==typeof o?o:1,u=d=null);var h=function(){l.animate(i.animIn,a,u,function(){c()})};r.animate(i.animOut,f,d,function(){r.css(i.cssAfter),i.sync||h()}),i.sync&&h()},e.fn.cycle.transitions={fade:function(t,n,i){n.not(":eq("+i.currSlide+")").css("opacity",0),i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i),i.cssBefore.opacity=0}),i.animIn={opacity:1},i.animOut={opacity:0},i.cssBefore={top:0,left:0}}},e.fn.cycle.ver=function(){return g},e.fn.cycle.defaults={activePagerClass:"activeSlide",after:null,allowPagerClickBubble:!1,animIn:null,animOut:null,aspect:!1,autostop:0,autostopCount:0,backwards:!1,before:null,center:null,cleartype:!e.support.opacity,cleartypeNoBg:!1,containerResize:1,containerResizeHeight:0,continuous:0,cssAfter:null,cssBefore:null,delay:0,easeIn:null,easeOut:null,easing:null,end:null,fastOnEvent:0,fit:0,fx:"fade",fxFn:null,height:"auto",manualTrump:!0,metaAttr:"cycle",next:null,nowrap:0,onPagerEvent:null,onPrevNextEvent:null,pager:null,pagerAnchorBuilder:null,pagerEvent:"click.cycle",pause:0,pauseOnPagerHover:0,prev:null,prevNextEvent:"click.cycle",random:0,randomizeEffects:1,requeueOnImageNotLoaded:!0,requeueTimeout:250,rev:0,shuffle:null,skipInitializationCallbacks:!1,slideExpr:null,slideResize:1,speed:1e3,speedIn:null,speedOut:null,startingSlide:t,sync:1,timeout:4e3,timeoutFn:null,updateActivePagerLink:null,width:null}}(jQuery),function(e){"use strict";e.fn.cycle.transitions.none=function(t,n,i){i.fxFn=function(t,n,i,c){e(n).show(),e(t).hide(),c()}},e.fn.cycle.transitions.fadeout=function(t,n,i){n.not(":eq("+i.currSlide+")").css({display:"block",opacity:1}),i.before.push(function(t,n,i,c,s,o){e(t).css("zIndex",i.slideCount+(o!==!0?1:0)),e(n).css("zIndex",i.slideCount+(o!==!0?0:1))}),i.animIn.opacity=1,i.animOut.opacity=0,i.cssBefore.opacity=1,i.cssBefore.display="block",i.cssAfter.zIndex=0},e.fn.cycle.transitions.scrollUp=function(t,n,i){t.css("overflow","hidden"),i.before.push(e.fn.cycle.commonReset);var c=t.height();i.cssBefore.top=c,i.cssBefore.left=0,i.cssFirst.top=0,i.animIn.top=0,i.animOut.top=-c},e.fn.cycle.transitions.scrollDown=function(t,n,i){t.css("overflow","hidden"),i.before.push(e.fn.cycle.commonReset);var c=t.height();i.cssFirst.top=0,i.cssBefore.top=-c,i.cssBefore.left=0,i.animIn.top=0,i.animOut.top=c},e.fn.cycle.transitions.scrollLeft=function(t,n,i){t.css("overflow","hidden"),i.before.push(e.fn.cycle.commonReset);var c=t.width();i.cssFirst.left=0,i.cssBefore.left=c,i.cssBefore.top=0,i.animIn.left=0,i.animOut.left=0-c},e.fn.cycle.transitions.scrollRight=function(t,n,i){t.css("overflow","hidden"),i.before.push(e.fn.cycle.commonReset);var c=t.width();i.cssFirst.left=0,i.cssBefore.left=-c,i.cssBefore.top=0,i.animIn.left=0,i.animOut.left=c},e.fn.cycle.transitions.scrollHorz=function(t,n,i){t.css("overflow","hidden").width(),i.before.push(function(t,n,i,c){i.rev&&(c=!c),e.fn.cycle.commonReset(t,n,i),i.cssBefore.left=c?n.cycleW-1:1-n.cycleW,i.animOut.left=c?-t.cycleW:t.cycleW}),i.cssFirst.left=0,i.cssBefore.top=0,i.animIn.left=0,i.animOut.top=0},e.fn.cycle.transitions.scrollVert=function(t,n,i){t.css("overflow","hidden"),i.before.push(function(t,n,i,c){i.rev&&(c=!c),e.fn.cycle.commonReset(t,n,i),i.cssBefore.top=c?1-n.cycleH:n.cycleH-1,i.animOut.top=c?t.cycleH:-t.cycleH}),i.cssFirst.top=0,i.cssBefore.left=0,i.animIn.top=0,i.animOut.left=0},e.fn.cycle.transitions.slideX=function(t,n,i){i.before.push(function(t,n,i){e(i.elements).not(t).hide(),e.fn.cycle.commonReset(t,n,i,!1,!0),i.animIn.width=n.cycleW}),i.cssBefore.left=0,i.cssBefore.top=0,i.cssBefore.width=0,i.animIn.width="show",i.animOut.width=0},e.fn.cycle.transitions.slideY=function(t,n,i){i.before.push(function(t,n,i){e(i.elements).not(t).hide(),e.fn.cycle.commonReset(t,n,i,!0,!1),i.animIn.height=n.cycleH}),i.cssBefore.left=0,i.cssBefore.top=0,i.cssBefore.height=0,i.animIn.height="show",i.animOut.height=0},e.fn.cycle.transitions.shuffle=function(t,n,i){var c,s=t.css("overflow","visible").width();for(n.css({left:0,top:0}),i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!0,!0,!0)}),i.speedAdjusted||(i.speed=i.speed/2,i.speedAdjusted=!0),i.random=0,i.shuffle=i.shuffle||{left:-s,top:15},i.els=[],c=0;c<n.length;c++)i.els.push(n[c]);for(c=0;c<i.currSlide;c++)i.els.push(i.els.shift());i.fxFn=function(t,n,i,c,s){i.rev&&(s=!s);var o=e(s?t:n);e(n).css(i.cssBefore);var r=i.slideCount;o.animate(i.shuffle,i.speedIn,i.easeIn,function(){for(var n=e.fn.cycle.hopsFromLast(i,s),l=0;n>l;l++)s?i.els.push(i.els.shift()):i.els.unshift(i.els.pop());if(s)for(var a=0,f=i.els.length;f>a;a++)e(i.els[a]).css("z-index",f-a+r);else{var u=e(t).css("z-index");o.css("z-index",parseInt(u,10)+1+r)}o.animate({left:0,top:0},i.speedOut,i.easeOut,function(){e(s?this:t).hide(),c&&c()})})},e.extend(i.cssBefore,{display:"block",opacity:1,top:0,left:0})},e.fn.cycle.transitions.turnUp=function(t,n,i){i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!0,!1),i.cssBefore.top=n.cycleH,i.animIn.height=n.cycleH,i.animOut.width=n.cycleW}),i.cssFirst.top=0,i.cssBefore.left=0,i.cssBefore.height=0,i.animIn.top=0,i.animOut.height=0},e.fn.cycle.transitions.turnDown=function(t,n,i){i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!0,!1),i.animIn.height=n.cycleH,i.animOut.top=t.cycleH}),i.cssFirst.top=0,i.cssBefore.left=0,i.cssBefore.top=0,i.cssBefore.height=0,i.animOut.height=0},e.fn.cycle.transitions.turnLeft=function(t,n,i){i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!1,!0),i.cssBefore.left=n.cycleW,i.animIn.width=n.cycleW}),i.cssBefore.top=0,i.cssBefore.width=0,i.animIn.left=0,i.animOut.width=0},e.fn.cycle.transitions.turnRight=function(t,n,i){i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!1,!0),i.animIn.width=n.cycleW,i.animOut.left=t.cycleW}),e.extend(i.cssBefore,{top:0,left:0,width:0}),i.animIn.left=0,i.animOut.width=0},e.fn.cycle.transitions.zoom=function(t,n,i){i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!1,!1,!0),i.cssBefore.top=n.cycleH/2,i.cssBefore.left=n.cycleW/2,e.extend(i.animIn,{top:0,left:0,width:n.cycleW,height:n.cycleH}),e.extend(i.animOut,{width:0,height:0,top:t.cycleH/2,left:t.cycleW/2})}),i.cssFirst.top=0,i.cssFirst.left=0,i.cssBefore.width=0,i.cssBefore.height=0},e.fn.cycle.transitions.fadeZoom=function(t,n,i){i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!1,!1),i.cssBefore.left=n.cycleW/2,i.cssBefore.top=n.cycleH/2,e.extend(i.animIn,{top:0,left:0,width:n.cycleW,height:n.cycleH})}),i.cssBefore.width=0,i.cssBefore.height=0,i.animOut.opacity=0},e.fn.cycle.transitions.blindX=function(t,n,i){var c=t.css("overflow","hidden").width();i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i),i.animIn.width=n.cycleW,i.animOut.left=t.cycleW}),i.cssBefore.left=c,i.cssBefore.top=0,i.animIn.left=0,i.animOut.left=c},e.fn.cycle.transitions.blindY=function(t,n,i){var c=t.css("overflow","hidden").height();i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i),i.animIn.height=n.cycleH,i.animOut.top=t.cycleH}),i.cssBefore.top=c,i.cssBefore.left=0,i.animIn.top=0,i.animOut.top=c},e.fn.cycle.transitions.blindZ=function(t,n,i){var c=t.css("overflow","hidden").height(),s=t.width();i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i),i.animIn.height=n.cycleH,i.animOut.top=t.cycleH}),i.cssBefore.top=c,i.cssBefore.left=s,i.animIn.top=0,i.animIn.left=0,i.animOut.top=c,i.animOut.left=s},e.fn.cycle.transitions.growX=function(t,n,i){i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!1,!0),i.cssBefore.left=this.cycleW/2,i.animIn.left=0,i.animIn.width=this.cycleW,i.animOut.left=0}),i.cssBefore.top=0,i.cssBefore.width=0},e.fn.cycle.transitions.growY=function(t,n,i){i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!0,!1),i.cssBefore.top=this.cycleH/2,i.animIn.top=0,i.animIn.height=this.cycleH,i.animOut.top=0}),i.cssBefore.height=0,i.cssBefore.left=0},e.fn.cycle.transitions.curtainX=function(t,n,i){i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!1,!0,!0),i.cssBefore.left=n.cycleW/2,i.animIn.left=0,i.animIn.width=this.cycleW,i.animOut.left=t.cycleW/2,i.animOut.width=0}),i.cssBefore.top=0,i.cssBefore.width=0},e.fn.cycle.transitions.curtainY=function(t,n,i){i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!0,!1,!0),i.cssBefore.top=n.cycleH/2,i.animIn.top=0,i.animIn.height=n.cycleH,i.animOut.top=t.cycleH/2,i.animOut.height=0}),i.cssBefore.height=0,i.cssBefore.left=0},e.fn.cycle.transitions.cover=function(t,n,i){var c=i.direction||"left",s=t.css("overflow","hidden").width(),o=t.height();i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i),i.cssAfter.display="","right"==c?i.cssBefore.left=-s:"up"==c?i.cssBefore.top=o:"down"==c?i.cssBefore.top=-o:i.cssBefore.left=s}),i.animIn.left=0,i.animIn.top=0,i.cssBefore.top=0,i.cssBefore.left=0},e.fn.cycle.transitions.uncover=function(t,n,i){var c=i.direction||"left",s=t.css("overflow","hidden").width(),o=t.height();i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!0,!0,!0),"right"==c?i.animOut.left=s:"up"==c?i.animOut.top=-o:"down"==c?i.animOut.top=o:i.animOut.left=-s}),i.animIn.left=0,i.animIn.top=0,i.cssBefore.top=0,i.cssBefore.left=0},e.fn.cycle.transitions.toss=function(t,n,i){var c=t.css("overflow","visible").width(),s=t.height();i.before.push(function(t,n,i){e.fn.cycle.commonReset(t,n,i,!0,!0,!0),i.animOut.left||i.animOut.top?i.animOut.opacity=0:e.extend(i.animOut,{left:2*c,top:-s/2,opacity:0})}),i.cssBefore.left=0,i.cssBefore.top=0,i.animIn.left=0},e.fn.cycle.transitions.wipe=function(t,n,i){var c=t.css("overflow","hidden").width(),s=t.height();i.cssBefore=i.cssBefore||{};var o;if(i.clip)if(/l2r/.test(i.clip))o="rect(0px 0px "+s+"px 0px)";else if(/r2l/.test(i.clip))o="rect(0px "+c+"px "+s+"px "+c+"px)";else if(/t2b/.test(i.clip))o="rect(0px "+c+"px 0px 0px)";else if(/b2t/.test(i.clip))o="rect("+s+"px "+c+"px "+s+"px 0px)";else if(/zoom/.test(i.clip)){var r=parseInt(s/2,10),l=parseInt(c/2,10);o="rect("+r+"px "+l+"px "+r+"px "+l+"px)"}i.cssBefore.clip=i.cssBefore.clip||o||"rect(0px 0px 0px 0px)";var a=i.cssBefore.clip.match(/(\d+)/g),f=parseInt(a[0],10),u=parseInt(a[1],10),d=parseInt(a[2],10),h=parseInt(a[3],10);i.before.push(function(t,n,i){if(t!=n){var o=e(t),r=e(n);e.fn.cycle.commonReset(t,n,i,!0,!0,!1),i.cssAfter.display="block";var l=1,a=parseInt(i.speedIn/13,10)-1;!function p(){var e=f?f-parseInt(l*(f/a),10):0,t=h?h-parseInt(l*(h/a),10):0,n=s>d?d+parseInt(l*((s-d)/a||1),10):s,i=c>u?u+parseInt(l*((c-u)/a||1),10):c;r.css({clip:"rect("+e+"px "+i+"px "+n+"px "+t+"px)"}),l++<=a?setTimeout(p,13):o.css("display","none")}()}}),e.extend(i.cssBefore,{display:"block",opacity:1,top:0,left:0}),i.animIn={left:0},i.animOut={left:0}}}(jQuery); \ No newline at end of file
diff --git a/plugins/jetpack/modules/shortcodes/js/main.js b/plugins/jetpack/modules/shortcodes/js/main.js
new file mode 100644
index 00000000..aecfdf60
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/js/main.js
@@ -0,0 +1,258 @@
+( function( $ ) {
+ var jmpressOpts = {
+ fullscreen: false,
+ hash: { use: false },
+ mouse: { clickSelects: false },
+ keyboard: { use: true },
+ animation: { transitionDuration: '1s' },
+ presentationMode: false,
+ stepSelector: '.step',
+ duration: {
+ defaultValue: 0,
+ },
+ };
+
+ /**
+ * Presentation constructor
+ */
+ function Presentation( wrapper ) {
+ var _self, duration, new_css, ie_regex, matches;
+
+ _self = this;
+
+ _self.wrapper = $( wrapper ); // The wrapper for toggling fullscreen
+ _self.slideshow = $( '.presentation', wrapper ); // Holds the slides for jmpress
+ _self.navLeft = $( '.nav-arrow-left', wrapper );
+ _self.navRight = $( '.nav-arrow-right', wrapper );
+ _self.expandButton = $( '.nav-fullscreen-button', wrapper );
+ _self.overlay = $( '.autoplay-overlay', wrapper );
+ _self.fullscreen = false;
+ _self.autoPlaying = false;
+ _self.autoplayTime = parseFloat( _self.slideshow.attr( 'data-autoplay' ), 10 ) || 0;
+
+ // The wrapper is scaled to the contents' size so that its border wraps tightly
+ _self.wrapper.css( {
+ width: _self.slideshow.width(),
+ height: _self.slideshow.height(),
+ } );
+
+ duration = _self.slideshow.attr( 'duration' ) || '1s';
+ jmpressOpts.animation.transitionDuration = duration;
+
+ // Compensate for transition times
+ if ( _self.autoplayTime ) {
+ _self.autoplayTime += parseFloat( duration, 10 ) * 1000;
+ }
+
+ // Set the opacity transition duration
+ // as it is delegated by css and not jmpress
+ duration = 'opacity ' + duration;
+ new_css = {
+ width: _self.slideshow.width(),
+ height: _self.slideshow.height(),
+ '-webkit-transition': duration,
+ '-moz-transition': duration,
+ '-ms-transition': duration,
+ '-o-transition': duration,
+ transition: duration,
+ };
+
+ $( '.step', _self.slideshow ).each( function( i, step ) {
+ $( step ).css( new_css );
+ } );
+
+ // Apply attribute to allow fading individual bullets here,
+ // otherwise wp_kses will strip the attribute out
+ $( '.step.fadebullets li', _self.slideshow ).each( function( i, step ) {
+ $( step ).attr( 'data-jmpress', 'fade' );
+ } );
+
+ // Register resizing to window when fullscreen
+ $( window ).resize( function() {
+ if ( _self.fullscreen ) {
+ _self.resizePresentation();
+ }
+ } );
+
+ // Register the nav bars to move the slides
+ _self.navLeft.on( 'click', function() {
+ _self.slideshow.jmpress( 'prev' );
+ _self.overlay.css( 'opacity', 0 );
+ return false;
+ } );
+
+ _self.navRight.on( 'click', function() {
+ _self.slideshow.jmpress( 'next' );
+ _self.overlay.css( 'opacity', 0 );
+ return false;
+ } );
+
+ _self.slideshow.on( 'click', function() {
+ _self.setAutoplay( true );
+ return false;
+ } );
+
+ _self.slideshow.on( 'focusout', function() {
+ _self.setAutoplay( false );
+ } );
+
+ // Register toggling fullscreen except for IE 9 or lower
+ ie_regex = /MSIE\s(\d+)\.\d+/;
+ matches = ie_regex.exec( navigator.userAgent );
+
+ if ( matches && parseInt( matches[ 1 ], 10 ) < 10 ) {
+ _self.expandButton.remove();
+ _self.expandButton = null;
+ } else {
+ _self.expandButton.on( 'click', function() {
+ _self.setFullscreen( ! _self.fullscreen );
+ return false;
+ } );
+ }
+
+ // Register ESC key to exit fullscreen
+ $( window ).on( 'keydown', function( event ) {
+ if ( event.which === 27 ) {
+ _self.setFullscreen( false );
+ }
+ } );
+
+ // Start the presentation
+ _self.slideshow.jmpress( jmpressOpts );
+
+ // Make content visible and remove error message on jmpress success
+ if ( _self.slideshow.jmpress( 'initialized' ) ) {
+ _self.slideshow.css( 'display', '' );
+ _self.overlay.css( 'display', '' );
+ $( '.not-supported-msg', _self.wrapper ).remove();
+ }
+
+ // A bug in Firefox causes issues with the nav arrows appearing
+ // on hover in presentation mode. Explicitly disabling fullscreen
+ // on init seems to fix the issue
+ _self.setFullscreen( false );
+ }
+
+ $.extend( Presentation.prototype, {
+ resizePresentation: function() {
+ var scale, duration, settings, new_css, widthScale, heightScale;
+
+ // Set the animation duration to 0 during resizing
+ // so that there isn't an animation delay when scaling
+ // up the slide contents
+ settings = this.slideshow.jmpress( 'settings' );
+ duration = settings.animation.transitionDuration;
+
+ settings.animation.transitionDuration = '0s';
+ this.slideshow.jmpress( 'reselect' );
+
+ scale = 1;
+ new_css = {
+ top: 0,
+ left: 0,
+ zoom: 1,
+ };
+
+ // Expand the presentation to fill the lesser of the max width or height
+ // This avoids content moving past the window for certain window sizes
+ if ( this.fullscreen ) {
+ widthScale = $( window ).width() / this.slideshow.width();
+ heightScale = $( window ).height() / this.slideshow.height();
+
+ scale = Math.min( widthScale, heightScale );
+
+ new_css.top = ( $( window ).height() - scale * this.slideshow.height() ) / 2;
+ new_css.left = ( $( window ).width() - scale * this.slideshow.width() ) / 2;
+ }
+
+ // Firefox does not support the zoom property; IE does, but it does not work
+ // well like in webkit, so we manually transform and position the slideshow
+ if ( this.slideshow.css( '-moz-transform' ) || this.slideshow.css( '-ms-transform' ) ) {
+ // Firefox keeps the center of the element in place and expands outward
+ // so we must shift everything to compensate
+ new_css.top += ( ( scale - 1 ) * this.slideshow.height() ) / 2;
+ new_css.left += ( ( scale - 1 ) * this.slideshow.width() ) / 2;
+
+ scale = 'scale(' + scale + ')';
+
+ $.extend( new_css, {
+ '-moz-transform': scale,
+ '-ms-transform': scale,
+ transform: scale,
+ } );
+ } else {
+ // webkit scales everything with zoom so we need to offset the right amount
+ // so that the content is vertically centered after scaling effects
+ new_css.top /= scale;
+ new_css.left /= scale;
+ new_css.zoom = scale;
+ }
+
+ this.slideshow.css( new_css );
+
+ settings.animation.transitionDuration = duration;
+ this.slideshow.jmpress( 'reselect' );
+ },
+
+ setFullscreen: function( on ) {
+ this.fullscreen = on;
+ this.setAutoplay( false );
+
+ // Save the scroll positions before going into fullscreen mode
+ if ( on ) {
+ this.scrollVert = $( window ).scrollTop();
+ this.scrollHoriz = $( window ).scrollLeft();
+
+ // Chrome Bug: Force scroll to be at top
+ // otherwise the presentation can end up offscreen
+ $( window ).scrollTop( 0 );
+ $( window ).scrollLeft( 0 );
+ }
+
+ $( 'html' ).toggleClass( 'presentation-global-fullscreen', on );
+ $( 'body' ).toggleClass( 'presentation-global-fullscreen', on );
+
+ this.wrapper.toggleClass( 'presentation-wrapper-fullscreen', on );
+
+ this.wrapper.parents().each( function( i, e ) {
+ $( e ).toggleClass( 'presentation-wrapper-fullscreen-parent', on );
+ } );
+
+ this.resizePresentation();
+
+ // Reset the scroll positions after exiting fullscreen mode
+ if ( ! on ) {
+ $( window ).scrollTop( this.scrollVert );
+ $( window ).scrollLeft( this.scrollHoriz );
+ }
+ },
+
+ setAutoplay: function( on ) {
+ var _self = this,
+ newAutoplayTime;
+
+ if ( _self.autoPlaying === on ) {
+ return;
+ }
+
+ newAutoplayTime = on && _self.autoplayTime > 0 ? _self.autoplayTime : 0;
+ _self.slideshow.jmpress( 'settings' ).duration.defaultValue = newAutoplayTime;
+
+ // Move to the next slide when activating autoplay
+ if ( newAutoplayTime ) {
+ _self.slideshow.jmpress( 'next' );
+ _self.overlay.css( 'opacity', 0 );
+ } else {
+ _self.slideshow.jmpress( 'reselect' );
+ }
+
+ _self.autoPlaying = on;
+ },
+ } );
+
+ $( document ).ready( function() {
+ $( '.presentation-wrapper' ).map( function() {
+ new Presentation( this );
+ } );
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/shortcodes/js/quiz.js b/plugins/jetpack/modules/shortcodes/js/quiz.js
new file mode 100644
index 00000000..6ab6e1d1
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/js/quiz.js
@@ -0,0 +1,67 @@
+( function( $ ) {
+ $.fn.shuffleQuiz = function() {
+ var allElems = this.get(),
+ getRandom = function( max ) {
+ return Math.floor( Math.random() * max );
+ },
+ shuffled = $.map( allElems, function() {
+ var random = getRandom( allElems.length ),
+ randEl = $( allElems[ random ] ).clone( true )[ 0 ];
+ allElems.splice( random, 1 );
+ return randEl;
+ } );
+
+ this.each( function( i ) {
+ $( this ).replaceWith( $( shuffled[ i ] ) );
+ } );
+
+ return $( shuffled );
+ };
+} )( jQuery );
+
+jQuery( function( $ ) {
+ $( '.jetpack-quiz' ).each( function() {
+ var quiz = $( this );
+ quiz.find( 'div.jetpack-quiz-answer' ).shuffleQuiz();
+ quiz
+ .find( 'div[data-correct]' )
+ .removeAttr( 'data-correct' )
+ .data( 'correct', 1 );
+ quiz.find( 'div.jetpack-quiz-answer:last' ).addClass( 'last' );
+ } );
+
+ $( 'div.jetpack-quiz' ).on( 'click', 'div.jetpack-quiz-answer', function() {
+ var trackid,
+ answer = $( this ),
+ quiz = answer.closest( 'div.jetpack-quiz' );
+
+ if ( quiz.data( 'a8ctraining' ) ) {
+ new Image().src =
+ '//pixel.wp.com/b.gif?v=wpcom-no-pv&x_trainingchaos-' +
+ quiz.data( 'username' ) +
+ '=' +
+ quiz.data( 'a8ctraining' ) +
+ '&rand=' +
+ Math.random();
+ quiz.data( 'a8ctraining', false );
+ quiz.data( 'trackid', false );
+ }
+
+ trackid = quiz.data( 'trackid' );
+ if ( answer.data( 'correct' ) ) {
+ answer.addClass( 'correct' );
+ if ( trackid ) {
+ new Image().src =
+ '//pixel.wp.com/b.gif?v=wpcom-no-pv&x_quiz-' + trackid + '=correct&rand=' + Math.random();
+ }
+ } else {
+ answer.addClass( 'wrong' );
+ if ( trackid ) {
+ new Image().src =
+ '//pixel.wp.com/b.gif?v=wpcom-no-pv&x_quiz-' + trackid + '=wrong&rand=' + Math.random();
+ }
+ }
+ // only track the first answer
+ quiz.data( 'trackid', false );
+ } );
+} );
diff --git a/plugins/jetpack/modules/shortcodes/js/recipes-printthis.js b/plugins/jetpack/modules/shortcodes/js/recipes-printthis.js
new file mode 100644
index 00000000..e31fb24c
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/js/recipes-printthis.js
@@ -0,0 +1,294 @@
+// jshint ignore: start
+/*
+ * printThis v1.9.0
+ * @desc Printing plug-in for jQuery
+ * @author Jason Day
+ *
+ * Resources (based on) :
+ * jPrintArea: http://plugins.jquery.com/project/jPrintArea
+ * jqPrint: https://github.com/permanenttourist/jquery.jqprint
+ * Ben Nadal: http://www.bennadel.com/blog/1591-Ask-Ben-Print-Part-Of-A-Web-Page-With-jQuery.htm
+ *
+ * Licensed under the MIT licence:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * (c) Jason Day 2015
+ *
+ * Usage:
+ *
+ * $("#mySelector").printThis({
+ * debug: false, * show the iframe for debugging
+ * importCSS: true, * import page CSS
+ * importStyle: false, * import style tags
+ * printContainer: true, * grab outer container as well as the contents of the selector
+ * loadCSS: "path/to/my.css", * path to additional css file - us an array [] for multiple
+ * pageTitle: "", * add title to print page
+ * removeInline: false, * remove all inline styles from print elements
+ * printDelay: 333, * variable print delay
+ * header: null, * prefix to html
+ * footer: null, * postfix to html
+ * base: false, * preserve the BASE tag, or accept a string for the URL
+ * formValues: true * preserve input/form values
+ * canvas: false * copy canvas elements (experimental)
+ * doctypeString: '...' * enter a different doctype for older markup
+ * });
+ *
+ * Notes:
+ * - the loadCSS will load additional css (with or without @media print) into the iframe, adjusting layout
+ *
+ * jshint onevar: false, smarttabs: true, devel: true
+ */
+( function( $ ) {
+ var opt;
+ $.fn.printThis = function( options ) {
+ opt = $.extend( {}, $.fn.printThis.defaults, options );
+ var $element = this instanceof jQuery ? this : $( this );
+
+ var strFrameName = 'printThis-' + new Date().getTime();
+
+ if ( window.location.hostname !== document.domain && navigator.userAgent.match( /msie/i ) ) {
+ // Ugly IE hacks due to IE not inheriting document.domain from parent
+ // checks if document.domain is set by comparing the host name against document.domain
+ var iframeSrc =
+ 'javascript:document.write("<head><script>document.domain=\\"' +
+ document.domain +
+ '\\";</s' +
+ 'cript></head><body></body>")';
+ var printI = document.createElement( 'iframe' );
+ printI.name = 'printIframe';
+ printI.id = strFrameName;
+ printI.className = 'MSIE';
+ document.body.appendChild( printI );
+ printI.src = iframeSrc;
+ } else {
+ // other browsers inherit document.domain, and IE works if document.domain is not explicitly set
+ var $frame = $( "<iframe id='" + strFrameName + "' name='printIframe' />" );
+ $frame.appendTo( 'body' );
+ }
+
+ var $iframe = $( '#' + strFrameName );
+
+ // show frame if in debug mode
+ if ( ! opt.debug )
+ $iframe.css( {
+ position: 'absolute',
+ width: '0px',
+ height: '0px',
+ left: '-600px',
+ top: '-600px',
+ } );
+
+ // $iframe.ready() and $iframe.load were inconsistent between browsers
+ setTimeout( function() {
+ // Add doctype to fix the style difference between printing and render
+ function setDocType( $iframe, doctype ) {
+ var win, doc;
+ win = $iframe.get( 0 );
+ win = win.contentWindow || win.contentDocument || win;
+ doc = win.document || win.contentDocument || win;
+ doc.open();
+ doc.write( doctype );
+ doc.close();
+ }
+ if ( opt.doctypeString ) {
+ setDocType( $iframe, opt.doctypeString );
+ }
+
+ var $doc = $iframe.contents(),
+ $head = $doc.find( 'head' ),
+ $body = $doc.find( 'body' ),
+ $base = $( 'base' ),
+ baseURL;
+
+ // add base tag to ensure elements use the parent domain
+ if ( opt.base === true && $base.length > 0 ) {
+ // take the base tag from the original page
+ baseURL = $base.attr( 'href' );
+ } else if ( typeof opt.base === 'string' ) {
+ // An exact base string is provided
+ baseURL = opt.base;
+ } else {
+ // Use the page URL as the base
+ baseURL = document.location.protocol + '//' + document.location.host;
+ }
+
+ $head.append( '<base href="' + baseURL + '">' );
+
+ // import page stylesheets
+ if ( opt.importCSS )
+ $( 'link[rel=stylesheet]' ).each( function() {
+ var href = $( this ).attr( 'href' );
+ if ( href ) {
+ var media = $( this ).attr( 'media' ) || 'all';
+ $head.append(
+ "<link type='text/css' rel='stylesheet' href='" + href + "' media='" + media + "'>"
+ );
+ }
+ } );
+
+ // import style tags
+ if ( opt.importStyle )
+ $( 'style' ).each( function() {
+ $( this )
+ .clone()
+ .appendTo( $head );
+ } );
+
+ // add title of the page
+ if ( opt.pageTitle ) $head.append( '<title>' + opt.pageTitle + '</title>' );
+
+ // import additional stylesheet(s)
+ if ( opt.loadCSS ) {
+ if ( $.isArray( opt.loadCSS ) ) {
+ jQuery.each( opt.loadCSS, function( index, value ) {
+ $head.append( "<link type='text/css' rel='stylesheet' href='" + this + "'>" );
+ } );
+ } else {
+ $head.append( "<link type='text/css' rel='stylesheet' href='" + opt.loadCSS + "'>" );
+ }
+ }
+
+ // print header
+ if ( opt.header ) $body.append( opt.header );
+
+ if ( opt.canvas ) {
+ // add canvas data-ids for easy access after the cloning.
+ var canvasId = 0;
+ $element.find( 'canvas' ).each( function() {
+ $( this ).attr( 'data-printthis', canvasId++ );
+ } );
+ }
+
+ // grab $.selector as container
+ if ( opt.printContainer ) $body.append( $element.outer() );
+ // otherwise just print interior elements of container
+ else
+ $element.each( function() {
+ $body.append( $( this ).html() );
+ } );
+
+ if ( opt.canvas ) {
+ // Re-draw new canvases by referencing the originals
+ $body.find( 'canvas' ).each( function() {
+ var cid = $( this ).data( 'printthis' ),
+ $src = $( '[data-printthis="' + cid + '"]' );
+
+ this.getContext( '2d' ).drawImage( $src[ 0 ], 0, 0 );
+
+ // Remove the mark-up from the original
+ $src.removeData( 'printthis' );
+ } );
+ }
+
+ // capture form/field values
+ if ( opt.formValues ) {
+ // loop through inputs
+ var $input = $element.find( 'input' );
+ if ( $input.length ) {
+ $input.each( function() {
+ var $this = $( this ),
+ $name = $( this ).attr( 'name' ),
+ $checker = $this.is( ':checkbox' ) || $this.is( ':radio' ),
+ $iframeInput = $doc.find( 'input[name="' + $name + '"]' ),
+ $value = $this.val();
+
+ // order matters here
+ if ( ! $checker ) {
+ $iframeInput.val( $value );
+ } else if ( $this.is( ':checked' ) ) {
+ if ( $this.is( ':checkbox' ) ) {
+ $iframeInput.attr( 'checked', 'checked' );
+ } else if ( $this.is( ':radio' ) ) {
+ $doc
+ .find( 'input[name="' + $name + '"][value="' + $value + '"]' )
+ .attr( 'checked', 'checked' );
+ }
+ }
+ } );
+ }
+
+ // loop through selects
+ var $select = $element.find( 'select' );
+ if ( $select.length ) {
+ $select.each( function() {
+ var $this = $( this ),
+ $name = $( this ).attr( 'name' ),
+ $value = $this.val();
+ $doc.find( 'select[name="' + $name + '"]' ).val( $value );
+ } );
+ }
+
+ // loop through textareas
+ var $textarea = $element.find( 'textarea' );
+ if ( $textarea.length ) {
+ $textarea.each( function() {
+ var $this = $( this ),
+ $name = $( this ).attr( 'name' ),
+ $value = $this.val();
+ $doc.find( 'textarea[name="' + $name + '"]' ).val( $value );
+ } );
+ }
+ } // end capture form/field values
+
+ // remove inline styles
+ if ( opt.removeInline ) {
+ // $.removeAttr available jQuery 1.7+
+ if ( $.isFunction( $.removeAttr ) ) {
+ $doc.find( 'body *' ).removeAttr( 'style' );
+ } else {
+ $doc.find( 'body *' ).attr( 'style', '' );
+ }
+ }
+
+ // print "footer"
+ if ( opt.footer ) $body.append( opt.footer );
+
+ setTimeout( function() {
+ if ( $iframe.hasClass( 'MSIE' ) ) {
+ // check if the iframe was created with the ugly hack
+ // and perform another ugly hack out of neccessity
+ window.frames[ 'printIframe' ].focus();
+ $head.append( '<script> window.print(); </s' + 'cript>' );
+ } else {
+ // proper method
+ if ( document.queryCommandSupported( 'print' ) ) {
+ $iframe[ 0 ].contentWindow.document.execCommand( 'print', false, null );
+ } else {
+ $iframe[ 0 ].contentWindow.focus();
+ $iframe[ 0 ].contentWindow.print();
+ }
+ }
+
+ // remove iframe after print
+ if ( ! opt.debug ) {
+ setTimeout( function() {
+ $iframe.remove();
+ }, 1000 );
+ }
+ }, opt.printDelay );
+ }, 333 );
+ };
+
+ // defaults
+ $.fn.printThis.defaults = {
+ debug: false, // show the iframe for debugging
+ importCSS: true, // import parent page css
+ importStyle: false, // import style tags
+ printContainer: true, // print outer container/$.selector
+ loadCSS: '', // load an additional css file - load multiple stylesheets with an array []
+ pageTitle: '', // add title to print page
+ removeInline: false, // remove all inline styles
+ printDelay: 333, // variable print delay
+ header: null, // prefix to html
+ footer: null, // postfix to html
+ formValues: true, // preserve input/form values
+ canvas: false, // Copy canvas content (experimental)
+ base: false, // preserve the BASE tag, or accept a string for the URL
+ doctypeString: '<!DOCTYPE html>', // html doctype
+ };
+
+ // $.selector container
+ jQuery.fn.outer = function() {
+ return $( $( '<div></div>' ).html( this.clone() ) ).html();
+ };
+} )( jQuery );
diff --git a/plugins/jetpack/modules/shortcodes/js/recipes.js b/plugins/jetpack/modules/shortcodes/js/recipes.js
new file mode 100644
index 00000000..37215b4a
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/js/recipes.js
@@ -0,0 +1,16 @@
+/* global jetpack_recipes_vars */
+( function( $ ) {
+ $( window ).load( function() {
+ $( '.jetpack-recipe-print a' ).click( function( event ) {
+ event.preventDefault();
+
+ // Print the DIV.
+ $( this )
+ .closest( '.jetpack-recipe' )
+ .printThis( {
+ pageTitle: jetpack_recipes_vars.pageTitle,
+ loadCSS: jetpack_recipes_vars.loadCSS,
+ } );
+ } );
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/shortcodes/js/slideshow-shortcode.js b/plugins/jetpack/modules/shortcodes/js/slideshow-shortcode.js
new file mode 100644
index 00000000..44a2bd6f
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/js/slideshow-shortcode.js
@@ -0,0 +1,205 @@
+/* jshint onevar:false, loopfunc:true */
+/* global jetpackSlideshowSettings, escape */
+
+function JetpackSlideshow( element, transition, autostart ) {
+ this.element = element;
+ this.images = [];
+ this.controls = {};
+ this.transition = transition || 'fade';
+ this.autostart = autostart;
+}
+
+JetpackSlideshow.prototype.showLoadingImage = function( toggle ) {
+ if ( toggle ) {
+ this.loadingImage_ = document.createElement( 'div' );
+ this.loadingImage_.className = 'slideshow-loading';
+ var img = document.createElement( 'img' );
+ img.src = jetpackSlideshowSettings.spinner;
+ this.loadingImage_.appendChild( img );
+ this.loadingImage_.appendChild( this.makeZeroWidthSpan() );
+ this.element.append( this.loadingImage_ );
+ } else if ( this.loadingImage_ ) {
+ this.loadingImage_.parentNode.removeChild( this.loadingImage_ );
+ this.loadingImage_ = null;
+ }
+};
+
+JetpackSlideshow.prototype.init = function() {
+ this.showLoadingImage( true );
+
+ var self = this;
+ // Set up DOM.
+ for ( var i = 0; i < this.images.length; i++ ) {
+ var imageInfo = this.images[ i ];
+ var img = document.createElement( 'img' );
+ img.src = imageInfo.src;
+ img.title = typeof imageInfo.title !== 'undefined' ? imageInfo.title : '';
+ img.alt = typeof imageInfo.alt !== 'undefined' ? imageInfo.alt : '';
+ img.align = 'middle';
+ img.setAttribute( 'itemprop', 'image' );
+ img.nopin = 'nopin';
+ var caption = document.createElement( 'div' );
+ caption.className = 'slideshow-slide-caption';
+ caption.setAttribute( 'itemprop', 'caption description' );
+ caption.innerHTML = imageInfo.caption;
+ var container = document.createElement( 'div' );
+ container.className = 'slideshow-slide';
+ container.setAttribute( 'itemprop', 'associatedMedia' );
+ container.setAttribute( 'itemscope', '' );
+ container.setAttribute( 'itemtype', 'https://schema.org/ImageObject' );
+
+ // Hide loading image once first image has loaded.
+ if ( i === 0 ) {
+ if ( img.complete ) {
+ // IE, image in cache
+ setTimeout( function() {
+ self.finishInit_();
+ }, 1 );
+ } else {
+ jQuery( img ).load( function() {
+ self.finishInit_();
+ } );
+ }
+ }
+ container.appendChild( img );
+ // I'm not sure where these were coming from, but IE adds
+ // bad values for width/height for portrait-mode images
+ img.removeAttribute( 'width' );
+ img.removeAttribute( 'height' );
+ container.appendChild( this.makeZeroWidthSpan() );
+ container.appendChild( caption );
+ this.element.append( container );
+ }
+};
+
+JetpackSlideshow.prototype.makeZeroWidthSpan = function() {
+ var emptySpan = document.createElement( 'span' );
+ emptySpan.className = 'slideshow-line-height-hack';
+ // Having a NBSP makes IE act weird during transitions, but other
+ // browsers ignore a text node with a space in it as whitespace.
+ if ( -1 !== window.navigator.userAgent.indexOf( 'MSIE ' ) ) {
+ emptySpan.appendChild( document.createTextNode( ' ' ) );
+ } else {
+ emptySpan.innerHTML = '&nbsp;';
+ }
+ return emptySpan;
+};
+
+JetpackSlideshow.prototype.finishInit_ = function() {
+ this.showLoadingImage( false );
+ this.renderControls_();
+
+ var self = this;
+ if ( this.images.length > 1 ) {
+ // Initialize Cycle instance.
+ this.element.cycle( {
+ fx: this.transition,
+ prev: this.controls.prev,
+ next: this.controls.next,
+ timeout: jetpackSlideshowSettings.speed,
+ slideExpr: '.slideshow-slide',
+ onPrevNextEvent: function() {
+ return self.onCyclePrevNextClick_.apply( self, arguments );
+ },
+ } );
+
+ var slideshow = this.element;
+
+ if ( ! this.autostart ) {
+ slideshow.cycle( 'pause' );
+ jQuery( this.controls.stop ).removeClass( 'running' );
+ jQuery( this.controls.stop ).addClass( 'paused' );
+ }
+
+ jQuery( this.controls.stop ).click( function() {
+ var button = jQuery( this );
+ if ( ! button.hasClass( 'paused' ) ) {
+ slideshow.cycle( 'pause' );
+ button.removeClass( 'running' );
+ button.addClass( 'paused' );
+ } else {
+ button.addClass( 'running' );
+ button.removeClass( 'paused' );
+ slideshow.cycle( 'resume', true );
+ }
+ return false;
+ } );
+ } else {
+ this.element.children( ':first' ).show();
+ this.element.css( 'position', 'relative' );
+ }
+ this.initialized_ = true;
+};
+
+JetpackSlideshow.prototype.renderControls_ = function() {
+ if ( this.controlsDiv_ ) {
+ return;
+ }
+
+ var controlsDiv = document.createElement( 'div' );
+ controlsDiv.className = 'slideshow-controls';
+
+ var controls = [ 'prev', 'stop', 'next' ];
+ for ( var i = 0; i < controls.length; i++ ) {
+ var controlName = controls[ i ];
+ var a = document.createElement( 'a' );
+ a.href = '#';
+ controlsDiv.appendChild( a );
+ this.controls[ controlName ] = a;
+ }
+ this.element.append( controlsDiv );
+ this.controlsDiv_ = controlsDiv;
+};
+
+JetpackSlideshow.prototype.onCyclePrevNextClick_ = function( isNext, i /*, slideElement*/ ) {
+ // If blog_id not present don't track page views
+ if ( ! jetpackSlideshowSettings.blog_id ) {
+ return;
+ }
+
+ var postid = this.images[ i ].id;
+ var stats = new Image();
+ stats.src =
+ document.location.protocol +
+ '//pixel.wp.com/g.gif?host=' +
+ escape( document.location.host ) +
+ '&rand=' +
+ Math.random() +
+ '&blog=' +
+ jetpackSlideshowSettings.blog_id +
+ '&subd=' +
+ jetpackSlideshowSettings.blog_subdomain +
+ '&user_id=' +
+ jetpackSlideshowSettings.user_id +
+ '&post=' +
+ postid +
+ '&ref=' +
+ escape( document.location );
+};
+
+( function( $ ) {
+ function jetpack_slideshow_init() {
+ $( '.jetpack-slideshow-noscript' ).remove();
+
+ $( '.jetpack-slideshow' ).each( function() {
+ var container = $( this );
+
+ if ( container.data( 'processed' ) ) {
+ return;
+ }
+
+ var slideshow = new JetpackSlideshow(
+ container,
+ container.data( 'trans' ),
+ container.data( 'autostart' )
+ );
+ slideshow.images = container.data( 'gallery' );
+ slideshow.init();
+
+ container.data( 'processed', true );
+ } );
+ }
+
+ $( document ).ready( jetpack_slideshow_init );
+ $( 'body' ).on( 'post-load', jetpack_slideshow_init );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/shortcodes/kickstarter.php b/plugins/jetpack/modules/shortcodes/kickstarter.php
new file mode 100644
index 00000000..1e6664bb
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/kickstarter.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Kickstarter shortcode
+ *
+ * Usage:
+ * [kickstarter url="https://www.kickstarter.com/projects/peaktoplateau/yak-wool-baselayers-from-tibet-to-the-world" width="480" height=""]
+ *
+ * @package Jetpack
+ */
+
+add_shortcode( 'kickstarter', 'jetpack_kickstarter_shortcode' );
+add_filter( 'pre_kses', 'jetpack_kickstarter_embed_to_shortcode' );
+
+/**
+ * Parse shortcode arguments and render its output.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode parameters.
+ *
+ * @return string
+ */
+function jetpack_kickstarter_shortcode( $atts ) {
+ if ( empty( $atts['url'] ) ) {
+ return '';
+ }
+
+ $url = esc_url_raw( $atts['url'] );
+ if ( ! preg_match( '#^(www\.)?kickstarter\.com$#i', wp_parse_url( $url, PHP_URL_HOST ) ) ) {
+ return '<!-- Invalid Kickstarter URL -->';
+ }
+
+ global $wp_embed;
+ return $wp_embed->shortcode( $atts, $url );
+}
+
+/**
+ * Converts Kickstarter iframe embeds to a shortcode.
+ *
+ * EG: <iframe width="480" height="360" src="http://www.kickstarter.com/projects/deweymac/dewey-mac-kid-detective-book-make-diy-and-stem-spy/widget/video.html" frameborder="0" scrolling="no"> </iframe>
+ *
+ * @since 4.5.0
+ *
+ * @param string $content Entry content that possibly includes a Kickstarter embed.
+ *
+ * @return string
+ */
+function jetpack_kickstarter_embed_to_shortcode( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, 'www.kickstarter.com/projects' ) ) {
+ return $content;
+ }
+
+ $regexp = '!<iframe((?:\s+\w+=[\'"][^\'"]*[\'"])*)\s+src=[\'"](http://www\.kickstarter\.com/projects/[^/]+/[^/]+)/[^\'"]+[\'"]((?:\s+\w+=[\'"][^\'"]*[\'"])*)>[\s]*</iframe>!i';
+ $regexp_ent = str_replace( '&amp;#0*58;', '&amp;#0*58;|&#0*58;', htmlspecialchars( $regexp, ENT_NOQUOTES ) ); // phpcs:ignore
+
+ foreach ( array( 'regexp', 'regexp_ent' ) as $reg ) {
+ if ( ! preg_match_all( $$reg, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ $url = esc_url( $match[2] );
+
+ $params = $match[1] . $match[3];
+
+ if ( 'regexp_ent' === $reg ) {
+ $params = html_entity_decode( $params );
+ }
+
+ $params = wp_kses_hair( $params, array( 'http' ) );
+
+ $width = isset( $params['width'] ) ? (int) $params['width']['value'] : 0;
+
+ $shortcode = '[kickstarter url=' . $url . ( ( ! empty( $width ) ) ? " width=$width" : '' ) . ']';
+ $content = str_replace( $match[0], $shortcode, $content );
+ }
+ }
+
+ return $content;
+}
diff --git a/plugins/jetpack/modules/shortcodes/mailchimp.php b/plugins/jetpack/modules/shortcodes/mailchimp.php
new file mode 100644
index 00000000..ce815673
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/mailchimp.php
@@ -0,0 +1,226 @@
+<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * MailChimp Subscriber Popup Form shortcode
+ *
+ * Example:
+ * [mailchimp_subscriber_popup baseUrl="mc.us11.list-manage.com" uuid="1ca7856462585a934b8674c71" lid="2d24f1898b"]
+ *
+ * Embed code example:
+ * <script type="text/javascript" src="//downloads.mailchimp.com/js/signup-forms/popup/unique-methods/embed.js" data-dojo-config="usePlainJson: true, isDebug: false"></script><script type="text/javascript">window.dojoRequire(["mojo/signup-forms/Loader"], function(L) { L.start({"baseUrl":"mc.us11.list-manage.com","uuid":"1ca7856462585a934b8674c71","lid":"2d24f1898b","uniqueMethods":true}) })</script>
+ */
+
+/**
+ * Register [mailchimp_subscriber_popup] shortcode and add a filter to 'pre_kses' queue to reverse MailChimp embed to shortcode.
+ *
+ * @since 4.5.0
+ */
+function jetpack_mailchimp_subscriber_popup() {
+ add_shortcode(
+ 'mailchimp_subscriber_popup',
+ array(
+ 'MailChimp_Subscriber_Popup',
+ 'shortcode',
+ )
+ );
+ add_filter(
+ 'pre_kses',
+ array(
+ 'MailChimp_Subscriber_Popup',
+ 'reversal',
+ )
+ );
+}
+
+if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ add_action( 'init', 'jetpack_mailchimp_subscriber_popup' );
+} else {
+ jetpack_mailchimp_subscriber_popup();
+}
+
+/**
+ * Class MailChimp_Subscriber_Popup
+ *
+ * @since 4.5.0
+ */
+class MailChimp_Subscriber_Popup {
+
+ /**
+ * Regular expressions to reverse script tags to shortcodes.
+ *
+ * @var array
+ */
+ private static $reversal_regexes = array(
+ /* raw examplejs */
+ '/<script type="text\/javascript" src="(https?:)?\/\/downloads\.mailchimp\.com\/js\/signup-forms\/popup\/unique-methods\/embed\.js" data-dojo-config="([^"]*?)"><\/script><script type="text\/javascript">window.dojoRequire\(\["mojo\/signup-forms\/Loader"\]\, function\(L\) { L\.start\({([^}]*?)}\) }\)<\/script>/s', //phpcs:ignore
+ /* visual editor */
+ '/&lt;script type="text\/javascript" src="(https?:)?\/\/downloads\.mailchimp\.com\/js\/signup-forms\/popup\/unique-methods\/embed\.js" data-dojo-config="([^"]*?)"&gt;&lt;\/script&gt;&lt;script type="text\/javascript"&gt;window.dojoRequire\(\["mojo\/signup-forms\/Loader"]\, function\(L\) { L\.start\({([^}]*?)}\) }\)&lt;\/script&gt;/s',
+ );
+
+ /**
+ * Allowed configuration attributes. Used in reversal when checking allowed attributes.
+ *
+ * @var array
+ */
+ private static $allowed_config = array(
+ 'usePlainJson' => 'true',
+ 'isDebug' => 'false',
+ );
+
+ /**
+ * Allowed JS variables. Used in reversal to whitelist variables.
+ *
+ * @var array
+ */
+ private static $allowed_js_vars = array(
+ 'baseUrl',
+ 'uuid',
+ 'lid',
+ );
+
+ /**
+ * Runs the whole reversal.
+ *
+ * @since 4.5.0
+ *
+ * @param string $content Post Content.
+ *
+ * @return string Content with embeds replaced
+ */
+ public static function reversal( $content ) {
+ // Bail without the js src.
+ if ( ! is_string( $content ) || false === stripos( $content, 'downloads.mailchimp.com/js/signup-forms/popup/unique-methods/embed.js' ) ) {
+ return $content;
+ }
+
+ require_once ABSPATH . WPINC . '/class-json.php';
+ $wp_json = new Services_JSON();
+
+ // loop through our rules and find valid embeds.
+ foreach ( self::$reversal_regexes as $regex ) {
+
+ if ( ! preg_match_all( $regex, $content, $matches ) ) {
+ continue;
+ }
+
+ foreach ( $matches[3] as $index => $js_vars ) {
+ // the regex rule for a specific embed.
+ $replace_regex = sprintf( '#\s*%s\s*#', preg_quote( $matches[0][ $index ], '#' ) );
+
+ $attrs = $wp_json->decode( '{' . $js_vars . '}' );
+
+ if ( $matches[2][ $index ] ) {
+ $config_attrs = $wp_json->decode( '{' . $matches[2][ $index ] . '}' );
+ foreach ( $config_attrs as $key => $value ) {
+ $attrs->$key = ( 1 === $value ) ? 'true' : 'false';
+ }
+ }
+
+ $shortcode = self::build_shortcode_from_reversal_attrs( $attrs );
+
+ $content = preg_replace( $replace_regex, "\n\n$shortcode\n\n", $content );
+
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extras', 'html_to_shortcode', 'mailchimp_subscriber_popup' );
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Builds the actual shortcode based on passed in attributes.
+ *
+ * @since 4.5.0
+ *
+ * @param array $attrs A valid list of attributes (gets matched against self::$allowed_config and self::$allowed_js_vars).
+ *
+ * @return string
+ */
+ private static function build_shortcode_from_reversal_attrs( $attrs ) {
+ $shortcode = '[mailchimp_subscriber_popup ';
+
+ foreach ( $attrs as $key => $value ) {
+ // skip unsupported keys.
+ if (
+ ! in_array( $key, array_keys( self::$allowed_config ), true )
+ && ! in_array( $key, self::$allowed_js_vars, true )
+ ) {
+ continue;
+ }
+
+ $value = esc_attr( $value );
+ $shortcode .= "$key='$value' ";
+ }
+ return trim( $shortcode ) . ']';
+ }
+
+ /**
+ * Parses the shortcode back out to embedded information.
+ *
+ * @since 4.5.0
+ *
+ * @param array $lcase_attrs Lowercase shortcode attributes.
+ *
+ * @return string
+ */
+ public static function shortcode( $lcase_attrs ) {
+ static $displayed_once = false;
+
+ // Limit to one form per page load.
+ if ( $displayed_once ) {
+ return '';
+ }
+
+ if ( empty( $lcase_attrs ) ) {
+ return '<!-- Missing MailChimp baseUrl, uuid or lid -->';
+ }
+
+ $defaults = array_fill_keys( self::$allowed_js_vars, '' );
+ $defaults = array_merge( $defaults, self::$allowed_config );
+
+ // Convert $attrs back to proper casing since they come through in all lowercase.
+ $attrs = array();
+ foreach ( $defaults as $key => $value ) {
+ if ( array_key_exists( strtolower( $key ), $lcase_attrs ) ) {
+ $attrs[ $key ] = $lcase_attrs[ strtolower( $key ) ];
+ }
+ }
+ $attrs = array_map( 'esc_js', array_filter( shortcode_atts( $defaults, $attrs ) ) );
+
+ // Split config & js vars.
+ $js_vars = array();
+ $config_vars = array();
+ foreach ( $attrs as $key => $value ) {
+ if (
+ 'baseUrl' === $key
+ && (
+ ! preg_match( '#mc\.us\d+\.list-manage\d?\.com#', $value, $matches )
+ || $value !== $matches[0]
+ )
+ ) {
+ return '<!-- Invalid MailChimp baseUrl -->';
+ }
+
+ if ( in_array( $key, self::$allowed_js_vars, true ) ) {
+ $js_vars[ $key ] = $value;
+ } else {
+ $config_vars[] = "$key: $value";
+ }
+ }
+
+ // If one of these parameters is missing we can't render the form so exist.
+ if ( empty( $js_vars['baseUrl'] ) || empty( $js_vars['uuid'] ) || empty( $js_vars['lid'] ) ) {
+ return '<!-- Missing MailChimp baseUrl, uuid or lid -->';
+ }
+
+ // Add a uniqueMethods parameter if it is missing from the data we got from the embed code.
+ $js_vars['uniqueMethods'] = true;
+
+ /** This action is already documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'mailchimp_subscriber_popup', 'view' );
+
+ $displayed_once = true;
+
+ return "\n\n" . '<script type="text/javascript" data-dojo-config="' . esc_attr( implode( ', ', $config_vars ) ) . '">jQuery.getScript( "//downloads.mailchimp.com/js/signup-forms/popup/unique-methods/embed.js", function( data, textStatus, jqxhr ) { window.dojoRequire(["mojo/signup-forms/Loader"], function(L) { L.start(' . wp_json_encode( $js_vars ) . ') });} );</script>' . "\n\n";
+ }
+}
diff --git a/plugins/jetpack/modules/shortcodes/medium.php b/plugins/jetpack/modules/shortcodes/medium.php
new file mode 100644
index 00000000..02d02aaf
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/medium.php
@@ -0,0 +1,71 @@
+<?php
+
+// Embed support for Medium https://medium.com/p/3eaed64aed8a
+
+/**
+ * Faux-oembed support for Medium permalinks
+ *
+ * e.g.
+ * https://medium.com/help-center
+ * https://medium.com/@richroll
+ */
+wp_embed_register_handler( 'medium', '#^https?://medium.com/([a-zA-z0-9-_@]+)#', 'jetpack_embed_medium_oembed' );
+
+function jetpack_embed_medium_oembed( $matches, $attr, $url ) {
+ $attr = jetpack_embed_medium_args( $attr );
+ $attr['url'] = $url;
+
+ return jetpack_embed_medium_embed_html( $attr );
+}
+
+function jetpack_embed_medium_embed_html( $args ) {
+ $args = jetpack_embed_medium_args( $args );
+
+ if ( empty( $args['url'] ) ) {
+ return;
+ }
+
+ $args['type'] = jetpack_embed_medium_get_embed_type( $args['url'] );
+
+ return sprintf( '<script async src="https://static.medium.com/embed.js"></script><a class="m-%1$s" href="%2$s" target="_blank" data-width="%3$s" data-border="%4$s" data-collapsed="%5$s">View %1$s at Medium.com</a>', esc_attr( $args['type'] ), esc_url( $args['url'] ), esc_attr( $args['width'] ), esc_attr( $args['border'] ), esc_attr( $args['collapsed'] ) );
+}
+
+/**
+ * Shortcode support that allows passing in URL
+ *
+ * [medium url="https://medium.com/help-center" width="100%" border="false" collapsed="true"]
+ */
+add_shortcode( 'medium', 'jetpack_embed_medium_shortcode' );
+
+function jetpack_embed_medium_shortcode( $atts ) {
+ $atts = jetpack_embed_medium_args( $atts );
+
+ if ( ! empty( $atts['url'] ) ) {
+ global $wp_embed;
+ return $wp_embed->shortcode( $atts, $atts['url'] );
+ }
+}
+
+function jetpack_embed_medium_get_embed_type( $url ) {
+ $url_path = parse_url( $url, PHP_URL_PATH );
+ if ( preg_match( '/^\/@[\.\w]+$/', $url_path ) ) {
+ return 'profile';
+ } elseif ( preg_match( '/^\/[\da-zA-Z-]+$/', $url_path ) ) {
+ return 'collection';
+ }
+
+ return 'story';
+}
+
+function jetpack_embed_medium_args( $atts ) {
+ return shortcode_atts(
+ array(
+ 'url' => '',
+ 'width' => '400',
+ 'border' => true,
+ 'collapsed' => false,
+ ),
+ $atts,
+ 'medium'
+ );
+}
diff --git a/plugins/jetpack/modules/shortcodes/mixcloud.php b/plugins/jetpack/modules/shortcodes/mixcloud.php
new file mode 100644
index 00000000..3580a924
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/mixcloud.php
@@ -0,0 +1,75 @@
+<?php
+/*
+ * Mixcloud embeds
+ *
+ * examples:
+ * [mixcloud MalibuRum/play-6-kissy-sellouts-winter-sun-house-party-mix/ /]
+ * [mixcloud MalibuRum/play-6-kissy-sellouts-winter-sun-house-party-mix/ width=640 height=480 /]
+ * [mixcloud http://www.mixcloud.com/MalibuRum/play-6-kissy-sellouts-winter-sun-house-party-mix/ /]
+ * [mixcloud http://www.mixcloud.com/MalibuRum/play-6-kissy-sellouts-winter-sun-house-party-mix/ width=640 height=480 /]
+ * [mixcloud]http://www.mixcloud.com/MalibuRum/play-6-kissy-sellouts-winter-sun-house-party-mix/[/mixcloud]
+ * [mixcloud]MalibuRum/play-6-kissy-sellouts-winter-sun-house-party-mix/[/mixcloud]
+ * [mixcloud http://www.mixcloud.com/mat/playlists/classics/ width=660 height=208 hide_cover=1 hide_tracklist=1]
+*/
+
+// Register oEmbed provider
+// Example URL: http://www.mixcloud.com/oembed/?url=http://www.mixcloud.com/MalibuRum/play-6-kissy-sellouts-winter-sun-house-party-mix/
+wp_oembed_add_provider( '#https?://(?:www\.)?mixcloud\.com/\S*#i', 'https://www.mixcloud.com/oembed', true );
+
+// Register mixcloud shortcode
+add_shortcode( 'mixcloud', 'mixcloud_shortcode' );
+function mixcloud_shortcode( $atts, $content = null ) {
+
+ if ( empty( $atts[0] ) && empty( $content ) ) {
+ return '<!-- mixcloud error: invalid mixcloud resource -->';
+ }
+
+ $regular_expression = '/((?<=mixcloud\\.com\\/)[\\w-\\/]+$)|(^[\\w-\\/]+$)/i';
+ preg_match( $regular_expression, $content, $match );
+ if ( ! empty( $match ) ) {
+ $resource_id = trim( $match[0] );
+ } else {
+ preg_match( $regular_expression, $atts[0], $match );
+ if ( ! empty( $match ) ) {
+ $resource_id = trim( $match[0] );
+ }
+ }
+
+ if ( empty( $resource_id ) ) {
+ return '<!-- mixcloud error: invalid mixcloud resource -->';
+ }
+
+ $mixcloud_url = 'https://mixcloud.com/' . $resource_id;
+
+ $atts = shortcode_atts(
+ array(
+ 'width' => false,
+ 'height' => false,
+ 'color' => false,
+ 'light' => false,
+ 'dark' => false,
+ 'hide_tracklist' => false,
+ 'hide_cover' => false,
+ 'mini' => false,
+ 'hide_followers' => false,
+ 'hide_artwork' => false,
+ ),
+ $atts
+ );
+
+ // remove falsey values
+ $atts = array_filter( $atts );
+
+ $query_args = array( 'url' => $mixcloud_url );
+ $query_args = array_merge( $query_args, $atts );
+
+ $url = add_query_arg( urlencode_deep( $query_args ), 'https://www.mixcloud.com/oembed/' );
+ $mixcloud_response = wp_remote_get( $url, array( 'redirection' => 0 ) );
+ if ( is_wp_error( $mixcloud_response ) || 200 !== $mixcloud_response['response']['code'] || empty( $mixcloud_response['body'] ) ) {
+ return '<!-- mixcloud error: invalid mixcloud resource -->';
+ }
+
+ $response_body = json_decode( $mixcloud_response['body'] );
+
+ return $response_body->html;
+}
diff --git a/plugins/jetpack/modules/shortcodes/pinterest.php b/plugins/jetpack/modules/shortcodes/pinterest.php
new file mode 100644
index 00000000..caecc619
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/pinterest.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Pinterest embeds
+ *
+ * Based on "Board Widget" example here: http://business.pinterest.com/widget-builder/#code
+ */
+
+// Example URL: http://pinterest.com/pinterest/pin-pets/
+// Second Example URL: https://uk.pinterest.com/annsawesomepins/travel/
+wp_embed_register_handler(
+ 'pinterest',
+ '#'
+ . 'https?://'
+ . '(?:www\.)?'
+ . '(?:[a-z]{2}\.)?'
+ . 'pinterest\.[a-z.]+/'
+ . '([^/]+)'
+ . '(/[^/]+)?'
+ . '#',
+ 'pinterest_embed_handler'
+);
+
+function pinterest_embed_handler( $matches, $attr, $url ) {
+ // Pinterest's JS handles making the embed
+ $script_src = '//assets.pinterest.com/js/pinit.js';
+ wp_enqueue_script( 'pinterest-embed', $script_src, array(), false, true );
+
+ $path = parse_url( $url, PHP_URL_PATH );
+ if ( 0 === strpos( $path, '/pin/' ) ) {
+ $embed_type = 'embedPin';
+ } elseif ( preg_match( '#^/([^/]+)/?$#', $path ) ) {
+ $embed_type = 'embedUser';
+ } elseif ( preg_match( '#^/([^/]+)/([^/]+)/?$#', $path ) ) {
+ $embed_type = 'embedBoard';
+ } else {
+ if ( current_user_can( 'edit_posts' ) ) {
+ return __( 'Sorry, that Pinterest URL was not recognized.', 'jetpack' );
+ }
+ return;
+ }
+
+ $return = sprintf( '<a data-pin-do="%s" href="%s"></a>', esc_attr( $embed_type ), esc_url( $url ) );
+
+ // If we're generating an embed view for the WordPress Admin via ajax...
+ if ( doing_action( 'wp_ajax_parse-embed' ) ) {
+ $return .= sprintf( '<script src="%s"></script>', esc_url( $script_src ) );
+ }
+
+ return $return;
+}
diff --git a/plugins/jetpack/modules/shortcodes/polldaddy.php b/plugins/jetpack/modules/shortcodes/polldaddy.php
new file mode 100644
index 00000000..10a62d65
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/polldaddy.php
@@ -0,0 +1,4 @@
+<?php
+/**
+ * Deprecated alias for Crowdsignal.
+ */
diff --git a/plugins/jetpack/modules/shortcodes/presentations.php b/plugins/jetpack/modules/shortcodes/presentations.php
new file mode 100644
index 00000000..77089189
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/presentations.php
@@ -0,0 +1,465 @@
+<?php
+/*
+Plugin Name: Presentations
+Plugin URI: http://automattic.com/wordpress-plugins/
+Description: Presentations plugin based on the work done by <a href="http://darylkoop.com/">Daryl Koopersmith</a>. Powered by jmpress.js
+Version: 0.2
+Author: Automattic
+Author URI: http://automattic.com/wordpress-plugins/
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+
+/**
+ * Known issues:
+ *
+ * - IE 7/8 are not supported by jmpress and presentations will not work
+ * - IE 9 will not animate transitions at all, though it's possible to at least
+ * switch between slides.
+ * - Infinite Scroll themes will not load presentations properly unless the post
+ * happens to be on the first loaded page. The permalink page will function
+ * properly, however.
+ * - Exiting fullscreen mode will not properly reset the scroll locations in Safari
+ */
+
+
+/*
+HOW TO: How the plugin settings are organized and which features are supported.
+
+The entire presentation should be wrapped with a [presentation] shortcode, and every
+individual slide should be wrapped with a [slide] shortcode. Any settings supported
+by [slide] can be set into [presentation], which will apply that setting for the entire
+presentation unless overridden by individual slides.
+
+- [presentation] only settings:
+ - duration: transition durations, default is one second.
+ - height: content height, default is 400px
+ - width: content width, default is 550px
+ - autoplay: delay between transitions in seconds, default 3s
+ when set the presentation will automatically transition between slides
+ as long as the presentation remains in focus
+
+- [slide] settings:
+ - transition: specifies where the next slide will be placed relative
+ to the last one before it. Supported values are "up", "down"
+ "left", "right", or "none". Default value is "down".
+
+ - scale: scales the content relative to other slides, default value is one
+
+ - rotate: rotates the content by the specified degrees, default is zero
+
+ - fade: slides will fade in and out during transition. Values of "on" or
+ "true" will enable fading, while values of "no" or "false" will
+ disable it. Default value is "on"
+
+ - bgcolor: specifies a background color for the slides. Any CSS valid value
+ is permitted. Default color is transparent.
+
+ - bgimg: specifies an image url which will fill the background. Image is
+ set to fill the background 100% width and height
+
+ - fadebullets: any html <li> tags will start out with an opacity of 0 and any
+ subsequent slide transitions will show the bullets one by one
+*/
+
+if ( ! class_exists( 'Presentations' ) ) :
+ class Presentations {
+
+ private $presentation_settings;
+ private $presentation_initialized;
+ private $scripts_and_style_included;
+
+ /**
+ * Constructor
+ */
+ function __construct() {
+ $this->presentation_initialized = false;
+ $this->scripts_and_style_included = false;
+
+ // Registers shortcodes
+ add_action( 'wp_head', array( &$this, 'add_scripts' ), 1 );
+
+ add_shortcode( 'presentation', array( &$this, 'presentation_shortcode' ) );
+ add_shortcode( 'slide', array( &$this, 'slide_shortcode' ) );
+ }
+
+ function add_scripts() {
+ $this->scripts_and_style_included = false;
+
+ if ( empty( $GLOBALS['posts'] ) || ! is_array( $GLOBALS['posts'] ) ) {
+ return;
+ }
+
+ foreach ( $GLOBALS['posts'] as $p ) {
+ if ( has_shortcode( $p->post_content, 'presentation' ) ) {
+ $this->scripts_and_style_included = true;
+ break;
+ }
+ }
+
+ if ( ! $this->scripts_and_style_included ) {
+ return;
+ }
+
+ $plugin = plugin_dir_url( __FILE__ );
+ // Add CSS
+ wp_enqueue_style( 'presentations', $plugin . 'css/style.css' );
+ // Add JavaScript
+ wp_enqueue_script( 'jquery' );
+ wp_enqueue_script(
+ 'jmpress',
+ Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/jmpress.min.js', 'modules/shortcodes/js/jmpress.js' ),
+ array( 'jquery' ),
+ '0.4.5',
+ true
+ );
+ wp_enqueue_script(
+ 'presentations',
+ Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/main.min.js', 'modules/shortcodes/js/main.js' ),
+ array( 'jquery', 'jmpress' ),
+ false,
+ true
+ );
+ }
+
+ function presentation_shortcode( $atts, $content = '' ) {
+ // Mark that we've found a valid [presentation] shortcode
+ $this->presentation_initialized = true;
+
+ $atts = shortcode_atts(
+ array(
+ 'duration' => '',
+ 'height' => '',
+ 'width' => '',
+ 'bgcolor' => '',
+ 'bgimg' => '',
+ 'autoplay' => '',
+
+ // Settings
+ 'transition' => '',
+ 'scale' => '',
+ 'rotate' => '',
+ 'fade' => '',
+ 'fadebullets' => '',
+ ),
+ $atts,
+ 'presentation'
+ );
+
+ $this->presentation_settings = array(
+ 'transition' => 'down',
+ 'scale' => 1,
+ 'rotate' => 0,
+ 'fade' => 'on',
+ 'fadebullets' => 0,
+ 'last' => array(
+ 'x' => 0,
+ 'y' => 0,
+ 'scale' => 1,
+ 'rotate' => 0,
+ ),
+ );
+
+ // Set the presentation-wide settings
+ if ( '' != trim( $atts['transition'] ) ) {
+ $this->presentation_settings['transition'] = $atts['transition'];
+ }
+
+ if ( '' != trim( $atts['scale'] ) ) {
+ $this->presentation_settings['scale'] = floatval( $atts['scale'] );
+ }
+
+ if ( '' != trim( $atts['rotate'] ) ) {
+ $this->presentation_settings['rotate'] = floatval( $atts['rotate'] );
+ }
+
+ if ( '' != trim( $atts['fade'] ) ) {
+ $this->presentation_settings['fade'] = $atts['fade'];
+ }
+
+ if ( '' != trim( $atts['fadebullets'] ) ) {
+ $this->presentation_settings['fadebullets'] = $atts['fadebullets'];
+ }
+
+ // Set any settings the slides don't care about
+ if ( '' != trim( $atts['duration'] ) ) {
+ $duration = floatval( $atts['duration'] ) . 's';
+ } else {
+ $duration = '1s';
+ }
+
+ // Autoplay durations are set in milliseconds
+ if ( '' != trim( $atts['autoplay'] ) ) {
+ $autoplay = floatval( $atts['autoplay'] ) * 1000;
+ } else {
+ $autoplay = 0;
+ } // No autoplay
+
+ // Set the presentation size as specified or with some nicely sized dimensions
+ if ( '' != trim( $atts['width'] ) ) {
+ $this->presentation_settings['width'] = intval( $atts['width'] );
+ } else {
+ $this->presentation_settings['width'] = 480;
+ }
+
+ if ( '' != trim( $atts['height'] ) ) {
+ $this->presentation_settings['height'] = intval( $atts['height'] );
+ } else {
+ $this->presentation_settings['height'] = 370;
+ }
+
+ // Hide the content by default in case the scripts fail
+ $style = 'display: none; width: ' . $this->presentation_settings['width'] . 'px; height: ' . $this->presentation_settings['height'] . 'px;';
+
+ // Check for background color XOR background image
+ // Use a white background if nothing specified
+ if ( preg_match( '/https?\:\/\/[^\'"\s]*/', $atts['bgimg'], $matches ) ) {
+ $style .= ' background-image: url("' . esc_url( $matches[0] ) . '");';
+ } elseif ( '' != trim( $atts['bgcolor'] ) ) {
+ $style .= ' background-color: ' . esc_attr( $atts['bgcolor'] ) . ';';
+ } else {
+ $style .= ' background-color: #fff;';
+ }
+
+ // Not supported message style is inlined incase the style sheet doesn't get included
+ $out = "<section class='presentation-wrapper'>";
+ $out .= "<p class='not-supported-msg' style='display: inherit; padding: 25%; text-align: center;'>";
+ $out .= __( 'This slideshow could not be started. Try refreshing the page or viewing it in another browser.', 'jetpack' ) . '</p>';
+
+ // Bail out unless the scripts were added
+ if ( $this->scripts_and_style_included ) {
+ $out .= sprintf(
+ '<div class="presentation" duration="%s" data-autoplay="%s" style="%s">',
+ esc_attr( $duration ),
+ esc_attr( $autoplay ),
+ esc_attr( $style )
+ );
+ $out .= "<div class='nav-arrow-left'></div>";
+ $out .= "<div class='nav-arrow-right'></div>";
+ $out .= "<div class='nav-fullscreen-button'></div>";
+
+ if ( $autoplay ) {
+ $out .= '<div class="autoplay-overlay" style="display: none;"><p class="overlay-msg">';
+ $out .= __( 'Click to autoplay the presentation!', 'jetpack' );
+ $out .= '</p></div>';
+ }
+
+ $out .= do_shortcode( $content );
+ }
+
+ $out .= '</section>';
+
+ $this->presentation_initialized = false;
+
+ return $out;
+ }
+
+ function slide_shortcode( $atts, $content = '' ) {
+ // Bail out unless wrapped by a [presentation] shortcode
+ if ( ! $this->presentation_initialized ) {
+ return $content;
+ }
+
+ $atts = shortcode_atts(
+ array(
+ 'transition' => '',
+ 'scale' => '',
+ 'rotate' => '',
+ 'fade' => '',
+ 'fadebullets' => '',
+ 'bgcolor' => '',
+ 'bgimg' => '',
+ ),
+ $atts,
+ 'slide'
+ );
+
+ // Determine positioning based on transition
+ if ( '' == trim( $atts['transition'] ) ) {
+ $atts['transition'] = $this->presentation_settings['transition'];
+ }
+
+ // Setting the content scale
+ if ( '' == trim( $atts['scale'] ) ) {
+ $atts['scale'] = $this->presentation_settings['scale'];
+ }
+
+ if ( '' == trim( $atts['scale'] ) ) {
+ $scale = 1;
+ } else {
+ $scale = floatval( $atts['scale'] );
+ }
+
+ if ( $scale < 0 ) {
+ $scale *= -1;
+ }
+
+ // Setting the content rotation
+ if ( '' == trim( $atts['rotate'] ) ) {
+ $atts['rotate'] = $this->presentation_settings['rotate'];
+ }
+
+ if ( '' == trim( $atts['rotate'] ) ) {
+ $rotate = 0;
+ } else {
+ $rotate = floatval( $atts['rotate'] );
+ }
+
+ // Setting if the content should fade
+ if ( '' == trim( $atts['fade'] ) ) {
+ $atts['fade'] = $this->presentation_settings['fade'];
+ }
+
+ if ( 'on' == $atts['fade'] || 'true' == $atts['fade'] ) {
+ $fade = 'fade';
+ } else {
+ $fade = '';
+ }
+
+ // Setting if bullets should fade on step changes
+ if ( '' == trim( $atts['fadebullets'] ) ) {
+ $atts['fadebullets'] = $this->presentation_settings['fadebullets'];
+ }
+
+ if ( 'on' == $atts['fadebullets'] || 'true' == $atts['fadebullets'] ) {
+ $fadebullets = 'fadebullets';
+ } else {
+ $fadebullets = '';
+ }
+
+ $coords = $this->get_coords(
+ array(
+ 'transition' => $atts['transition'],
+ 'scale' => $scale,
+ 'rotate' => $rotate,
+ )
+ );
+
+ $x = $coords['x'];
+ $y = $coords['y'];
+
+ // Check for background color XOR background image
+ // Use a white background if nothing specified
+ if ( preg_match( '/https?\:\/\/[^\'"\s]*/', $atts['bgimg'], $matches ) ) {
+ $style = 'background-image: url("' . esc_url( $matches[0] ) . '");';
+ } elseif ( '' != trim( $atts['bgcolor'] ) ) {
+ $style = 'background-color: ' . esc_attr( $atts['bgcolor'] ) . ';';
+ } else {
+ $style = '';
+ }
+
+ // Put everything together and let jmpress do the magic!
+ $out = sprintf(
+ '<div class="step %s %s" data-x="%s" data-y="%s" data-scale="%s" data-rotate="%s" style="%s">',
+ esc_attr( $fade ),
+ esc_attr( $fadebullets ),
+ esc_attr( $x ),
+ esc_attr( $y ),
+ esc_attr( $scale ),
+ esc_attr( $rotate ),
+ esc_attr( $style )
+ );
+
+ $out .= '<div class="slide-content">';
+ $out .= do_shortcode( $content );
+ $out .= '</div></div>';
+
+ return $out;
+ }
+
+ /**
+ * Determines the position of the next slide based on the position and scaling of the previous slide.
+ *
+ * @param array $args : an array with the following key-value pairs
+ * string $transition: the transition name, "up", "down", "left", or "right"
+ * float $scale: the scale of the next slide (used to determine the position of the slide after that)
+ *
+ * @return array with the 'x' and 'y' coordinates of the slide
+ */
+ function get_coords( $args ) {
+ if ( 0 == $args['scale'] ) {
+ $args['scale'] = 1;
+ }
+
+ $width = $this->presentation_settings['width'];
+ $height = $this->presentation_settings['height'];
+ $last = $this->presentation_settings['last'];
+ $scale = $last['scale'];
+
+ $next = array(
+ 'x' => $last['x'],
+ 'y' => $last['y'],
+ 'scale' => $args['scale'],
+ 'rotate' => $args['rotate'],
+ );
+
+ // All angles are measured from the vertical axis, so everything is backwards!
+ $diagAngle = atan2( $width, $height );
+ $diagonal = sqrt( pow( $width, 2 ) + pow( $height, 2 ) );
+
+ // We offset the angles by the angle formed by the diagonal so that
+ // we can multiply the sines directly against the diagonal length
+ $theta = deg2rad( $last['rotate'] ) - $diagAngle;
+ $phi = deg2rad( $next['rotate'] ) - $diagAngle;
+
+ // We start by displacing by the slide dimensions
+ $totalHorizDisp = $width * $scale;
+ $totalVertDisp = $height * $scale;
+
+ // If the previous slide was rotated, we add the incremental offset from the rotation
+ // Namely the difference between the regular dimension (no rotation) and the component
+ // of the diagonal for that angle
+ $totalHorizDisp += ( ( ( abs( sin( $theta ) ) * $diagonal ) - $width ) / 2 ) * $scale;
+ $totalVertDisp += ( ( ( abs( cos( $theta ) ) * $diagonal ) - $height ) / 2 ) * $scale;
+
+ // Similarly, we check if the current slide has been rotated and add whatever additional
+ // offset has been added. This is so that two rotated corners don't clash with each other.
+ // Note: we are checking the raw angle relative to the vertical axis, NOT the diagonal angle.
+ if ( 0 !== $next['rotate'] % 180 ) {
+ $totalHorizDisp += ( abs( ( sin( $phi ) * $diagonal ) - $width ) / 2 ) * $next['scale'];
+ $totalVertDisp += ( abs( ( cos( $phi ) * $diagonal ) - $height ) / 2 ) * $next['scale'];
+ }
+
+ switch ( trim( $args['transition'] ) ) {
+ case 'none':
+ break;
+
+ case 'left':
+ $next['x'] -= $totalHorizDisp;
+ break;
+
+ case 'right':
+ $next['x'] += $totalHorizDisp;
+ break;
+
+ case 'up':
+ $next['y'] -= $totalVertDisp;
+ break;
+
+ case 'down':
+ default:
+ $next['y'] += $totalVertDisp;
+ break;
+ }
+
+ $this->presentation_settings['last'] = $next;
+
+ return $next;
+ }
+ }
+
+ $GLOBALS['presentations'] = new Presentations();
+endif;
diff --git a/plugins/jetpack/modules/shortcodes/quiz.php b/plugins/jetpack/modules/shortcodes/quiz.php
new file mode 100644
index 00000000..fa4ed960
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/quiz.php
@@ -0,0 +1,309 @@
+<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileNam
+/**
+ * Quiz shortcode.
+ *
+ * Usage:
+ *
+ * [quiz]
+ * [question]What's the right answer?[/question]
+ * [wrong]This one?[explanation]Nope[/explanation][/wrong]
+ * [answer]Yes, this is the one![explanation]Yay![/explanation][/answer]
+ * [wrong]Maybe this one[explanation]Keep trying[/explanation][/wrong]
+ * [wrong]How about this one?[explanation]Try again[/explanation][/wrong]
+ * [/quiz]
+ */
+class Quiz_Shortcode {
+
+ /**
+ * Parameters admitted by [quiz] shortcode.
+ *
+ * @since 4.5.0
+ *
+ * @var array
+ */
+ private static $quiz_params = array();
+
+ /**
+ * Whether the scripts were enqueued.
+ *
+ * @since 4.5.0
+ *
+ * @var bool
+ */
+ private static $scripts_enqueued = false;
+
+ /**
+ * In a8c training, store user currently logged in.
+ *
+ * @since 4.5.0
+ *
+ * @var null
+ */
+ private static $username = null;
+
+ /**
+ * Whether the noscript tag was already printed.
+ *
+ * @since 4.5.0
+ *
+ * @var bool
+ */
+ private static $noscript_info_printed = false;
+
+ /**
+ * Whether JavaScript is available.
+ *
+ * @since 4.5.0
+ *
+ * @var null
+ */
+ private static $javascript_unavailable = null;
+
+ /**
+ * Register all shortcodes.
+ *
+ * @since 4.5.0
+ */
+ public static function init() {
+ add_shortcode( 'quiz', array( __CLASS__, 'shortcode' ) );
+ add_shortcode( 'question', array( __CLASS__, 'question_shortcode' ) );
+ add_shortcode( 'answer', array( __CLASS__, 'answer_shortcode' ) );
+ add_shortcode( 'wrong', array( __CLASS__, 'wrong_shortcode' ) );
+ add_shortcode( 'explanation', array( __CLASS__, 'explanation_shortcode' ) );
+ }
+
+ /**
+ * Enqueue assets needed by the quiz,
+ *
+ * @since 4.5.0
+ */
+ private static function enqueue_scripts() {
+ wp_enqueue_style( 'quiz', plugins_url( 'css/quiz.css', __FILE__ ), array(), JETPACK__VERSION );
+ wp_enqueue_script(
+ 'quiz',
+ Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/quiz.min.js', 'modules/shortcodes/js/quiz.js' ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ true
+ );
+ }
+
+ /**
+ * Check if this is a feed and thus JS is unavailable.
+ *
+ * @since 4.5.0
+ *
+ * @return bool|null
+ */
+ private static function is_javascript_unavailable() {
+ if ( ! is_null( self::$javascript_unavailable ) ) {
+ return self::$javascript_unavailable;
+ }
+
+ if ( is_feed() ) {
+ self::$javascript_unavailable = true;
+ return self::$javascript_unavailable;
+ }
+
+ self::$javascript_unavailable = false;
+ return self::$javascript_unavailable;
+ }
+
+ /**
+ * Display message when JS is not available.
+ *
+ * @since 4.5.0
+ *
+ * @return string
+ */
+ private static function noscript_info() {
+ if ( self::$noscript_info_printed ) {
+ return '';
+ }
+ self::$noscript_info_printed = true;
+ return '<noscript><div><i>' . esc_html__( 'Please view this post in your web browser to complete the quiz.', 'jetpack' ) . '</i></div></noscript>';
+ }
+
+ /**
+ * Check if we're in WordPress.com.
+ *
+ * @since 4.5.0
+ *
+ * @return bool
+ */
+ public static function is_wpcom() {
+ return defined( 'IS_WPCOM' ) && IS_WPCOM;
+ }
+
+ /**
+ * Parse shortcode arguments and render its output.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode parameters.
+ * @param string $content Content enclosed by shortcode tags.
+ *
+ * @return string
+ */
+ public static function shortcode( $atts, $content = null ) {
+
+ // There's nothing to do if there's nothing enclosed.
+ if ( empty( $content ) ) {
+ return '';
+ }
+
+ $id = '';
+
+ if ( self::is_javascript_unavailable() ) {
+ // in an e-mail print the question and the info sentence once per question, too.
+ self::$noscript_info_printed = false;
+ } else {
+
+ if ( ! self::$scripts_enqueued ) {
+ // lazy enqueue cannot use the wp_enqueue_scripts action anymore.
+ self::enqueue_scripts();
+ self::$scripts_enqueued = true;
+ }
+
+ $default_atts = self::is_wpcom()
+ ? array(
+ 'trackid' => '',
+ 'a8ctraining' => '',
+ )
+ : array(
+ 'trackid' => '',
+ );
+
+ self::$quiz_params = shortcode_atts( $default_atts, $atts );
+
+ if ( ! empty( self::$quiz_params['trackid'] ) ) {
+ $id .= ' data-trackid="' . esc_attr( self::$quiz_params['trackid'] ) . '"';
+ }
+ if ( self::is_wpcom() && ! empty( self::$quiz_params['a8ctraining'] ) ) {
+ if ( is_null( self::$username ) ) {
+ self::$username = wp_get_current_user()->user_login;
+ }
+ $id .= ' data-a8ctraining="' . esc_attr( self::$quiz_params['a8ctraining'] ) . '" data-username="' . esc_attr( self::$username ) . '"';
+ }
+ }
+
+ $quiz = self::do_shortcode( $content );
+ return '<div class="jetpack-quiz quiz"' . $id . '>' . $quiz . '</div>';
+ }
+
+ /**
+ * Strip line breaks, restrict allowed HTML to a few whitelisted tags and execute nested shortcodes.
+ *
+ * @since 4.5.0
+ *
+ * @param string $content Post content.
+ *
+ * @return mixed|string
+ */
+ private static function do_shortcode( $content ) {
+ // strip autoinserted line breaks.
+ $content = preg_replace( '#(<(?:br /|/?p)>\n?)*(\[/?[a-z]+\])(<(?:br /|/?p)>\n?)*#', '$2', $content );
+
+ // Add internal parameter so it's only rendered when it has it.
+ $content = preg_replace( '/\[(question|answer|wrong|explanation)\]/i', '[$1 quiz_item="true"]', $content );
+ $content = do_shortcode( $content );
+ $content = wp_kses(
+ $content,
+ array(
+ 'tt' => array(),
+ 'a' => array( 'href' => true ),
+ 'pre' => array(),
+ 'strong' => array(),
+ 'i' => array(),
+ 'br' => array(),
+ 'img' => array( 'src' => true ),
+ 'div' => array(
+ 'class' => true,
+ 'data-correct' => 1,
+ 'data-track-id' => 1,
+ 'data-a8ctraining' => 1,
+ 'data-username' => 1,
+ ),
+ )
+ );
+ return $content;
+ }
+
+ /**
+ * Render question.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode attributes.
+ * @param null $content Post content.
+ *
+ * @return string
+ */
+ public static function question_shortcode( $atts, $content = null ) {
+ return isset( $atts['quiz_item'] )
+ ? '<div class="jetpack-quiz-question question">' . self::do_shortcode( $content ) . '</div>'
+ : '';
+ }
+
+ /**
+ * Render correct answer.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode attributes.
+ * @param null $content Post content.
+ *
+ * @return string
+ */
+ public static function answer_shortcode( $atts, $content = null ) {
+ if ( self::is_javascript_unavailable() ) {
+ return self::noscript_info();
+ }
+
+ return isset( $atts['quiz_item'] )
+ ? '<div class="jetpack-quiz-answer answer" data-correct="1">' . self::do_shortcode( $content ) . '</div>'
+ : '';
+ }
+
+ /**
+ * Render wrong response.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode attributes.
+ * @param null $content Post content.
+ *
+ * @return string
+ */
+ public static function wrong_shortcode( $atts, $content = null ) {
+ if ( self::is_javascript_unavailable() ) {
+ return self::noscript_info();
+ }
+
+ return isset( $atts['quiz_item'] )
+ ? '<div class="jetpack-quiz-answer answer">' . self::do_shortcode( $content ) . '</div>'
+ : '';
+ }
+
+ /**
+ * Render explanation for wrong or right answer.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode attributes.
+ * @param null $content Post content.
+ *
+ * @return string
+ */
+ public static function explanation_shortcode( $atts, $content = null ) {
+ if ( self::is_javascript_unavailable() ) {
+ return self::noscript_info();
+ }
+
+ return isset( $atts['quiz_item'] )
+ ? '<div class="jetpack-quiz-explanation explanation">' . self::do_shortcode( $content ) . '</div>'
+ : '';
+ }
+}
+
+Quiz_Shortcode::init();
diff --git a/plugins/jetpack/modules/shortcodes/recipe.php b/plugins/jetpack/modules/shortcodes/recipe.php
new file mode 100644
index 00000000..7846b154
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/recipe.php
@@ -0,0 +1,515 @@
+<?php
+
+/**
+ * Embed recipe 'cards' in post, with basic styling and print functionality
+ *
+ * To Do
+ * - defaults settings
+ * - basic styles/themecolor styles
+ * - validation/sanitization
+ * - print styles
+ */
+class Jetpack_Recipes {
+
+ private $scripts_and_style_included = false;
+
+ function __construct() {
+ add_action( 'init', array( $this, 'action_init' ) );
+
+ add_filter( 'wp_kses_allowed_html', array( $this, 'add_recipes_kses_rules' ), 10, 2 );
+ }
+
+ /**
+ * Add Schema-specific attributes to our allowed tags in wp_kses,
+ * so we can have better Schema.org compliance.
+ *
+ * @param array $allowedtags Array of allowed HTML tags in recipes.
+ * @param array $context Context to judge allowed tags by.
+ */
+ function add_recipes_kses_rules( $allowedtags, $context ) {
+ if ( in_array( $context, array( '', 'post', 'data' ) ) ) :
+ // Create an array of all the tags we'd like to add the itemprop attribute to.
+ $tags = array( 'li', 'ol', 'ul', 'img', 'p', 'h3', 'time' );
+ foreach ( $tags as $tag ) {
+ $allowedtags = $this->add_kses_rule(
+ $allowedtags,
+ $tag,
+ array(
+ 'class' => array(),
+ 'itemprop' => array(),
+ 'datetime' => array(),
+ )
+ );
+ }
+
+ // Allow itemscope and itemtype for divs.
+ $allowedtags = $this->add_kses_rule(
+ $allowedtags,
+ 'div',
+ array(
+ 'class' => array(),
+ 'itemscope' => array(),
+ 'itemtype' => array(),
+ )
+ );
+ endif;
+
+ return $allowedtags;
+ }
+
+ /**
+ * Function to add a new property rule to our kses array.
+ * Used by add_recipe_kses_rules() above.
+ *
+ * @param array $all_tags Array of allowed HTML tags in recipes.
+ * @param string $tag New HTML tag to add to the array of allowed HTML.
+ * @param array $rules Array of allowed attributes for that HTML tag.
+ */
+ private function add_kses_rule( $all_tags, $tag, $rules ) {
+
+ // If the tag doesn't already exist, add it.
+ if ( ! isset( $all_tags[ $tag ] ) ) {
+ $all_tags[ $tag ] = array();
+ }
+
+ // Merge the new tags with existing tags.
+ $all_tags[ $tag ] = array_merge( $all_tags[ $tag ], $rules );
+
+ return $all_tags;
+ }
+
+ /**
+ * Register our shortcode and enqueue necessary files.
+ */
+ function action_init() {
+ // Enqueue styles if [recipe] exists.
+ add_action( 'wp_head', array( $this, 'add_scripts' ), 1 );
+
+ // Render [recipe], along with other shortcodes that can be nested within.
+ add_shortcode( 'recipe', array( $this, 'recipe_shortcode' ) );
+ add_shortcode( 'recipe-notes', array( $this, 'recipe_notes_shortcode' ) );
+ add_shortcode( 'recipe-ingredients', array( $this, 'recipe_ingredients_shortcode' ) );
+ add_shortcode( 'recipe-directions', array( $this, 'recipe_directions_shortcode' ) );
+ }
+
+ /**
+ * Enqueue scripts and styles
+ */
+ function add_scripts() {
+ if ( empty( $GLOBALS['posts'] ) || ! is_array( $GLOBALS['posts'] ) ) {
+ return;
+ }
+
+ foreach ( $GLOBALS['posts'] as $p ) {
+ if ( has_shortcode( $p->post_content, 'recipe' ) ) {
+ $this->scripts_and_style_included = true;
+ break;
+ }
+ }
+
+ if ( ! $this->scripts_and_style_included ) {
+ return;
+ }
+
+ wp_enqueue_style( 'jetpack-recipes-style', plugins_url( '/css/recipes.css', __FILE__ ), array(), '20130919' );
+ wp_style_add_data( 'jetpack-recipes-style', 'rtl', 'replace' );
+
+ // add $themecolors-defined styles.
+ wp_add_inline_style( 'jetpack-recipes-style', self::themecolor_styles() );
+
+ wp_enqueue_script(
+ 'jetpack-recipes-printthis',
+ Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes-printthis.min.js', 'modules/shortcodes/js/recipes-printthis.js' ),
+ array( 'jquery' ),
+ '20170202'
+ );
+
+ wp_enqueue_script(
+ 'jetpack-recipes-js',
+ Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes.min.js', 'modules/shortcodes/js/recipes.js' ),
+ array( 'jquery', 'jetpack-recipes-printthis' ),
+ '20131230'
+ );
+
+ $title_var = wp_title( '|', false, 'right' );
+ $rtl = is_rtl() ? '-rtl' : '';
+ $print_css_var = plugins_url( "/css/recipes-print{$rtl}.css", __FILE__ );
+
+ wp_localize_script(
+ 'jetpack-recipes-js',
+ 'jetpack_recipes_vars',
+ array(
+ 'pageTitle' => $title_var,
+ 'loadCSS' => $print_css_var,
+ )
+ );
+ }
+
+ /**
+ * Our [recipe] shortcode.
+ * Prints recipe data styled to look good on *any* theme.
+ *
+ * @param array $atts Array of shortcode attributes.
+ * @param string $content Post content.
+ *
+ * @return string HTML for recipe shortcode.
+ */
+ static function recipe_shortcode( $atts, $content = '' ) {
+ $atts = shortcode_atts(
+ array(
+ 'title' => '', // string.
+ 'servings' => '', // intval.
+ 'time' => '', // string.
+ 'difficulty' => '', // string.
+ 'print' => '', // string.
+ 'source' => '', // string.
+ 'sourceurl' => '', // string.
+ 'image' => '', // string.
+ 'description' => '', // string.
+ ),
+ $atts,
+ 'recipe'
+ );
+
+ return self::recipe_shortcode_html( $atts, $content );
+ }
+
+ /**
+ * The recipe output
+ *
+ * @param array $atts Array of shortcode attributes.
+ * @param string $content Post content.
+ *
+ * @return string HTML output
+ */
+ static function recipe_shortcode_html( $atts, $content = '' ) {
+
+ $html = '<div class="hrecipe jetpack-recipe" itemscope itemtype="https://schema.org/Recipe">';
+
+ // Print the recipe title if exists.
+ if ( '' !== $atts['title'] ) {
+ $html .= '<h3 class="jetpack-recipe-title" itemprop="name">' . esc_html( $atts['title'] ) . '</h3>';
+ }
+
+ // Print the recipe meta if exists.
+ if ( '' !== $atts['servings'] || '' != $atts['time'] || '' != $atts['difficulty'] || '' != $atts['print'] ) {
+ $html .= '<ul class="jetpack-recipe-meta">';
+
+ if ( '' !== $atts['servings'] ) {
+ $html .= sprintf(
+ '<li class="jetpack-recipe-servings" itemprop="recipeYield"><strong>%1$s: </strong>%2$s</li>',
+ esc_html_x( 'Servings', 'recipe', 'jetpack' ),
+ esc_html( $atts['servings'] )
+ );
+ }
+
+ if ( '' !== $atts['time'] ) {
+ // Get a time that's supported by Schema.org.
+ $duration = WPCOM_JSON_API_Date::format_duration( $atts['time'] );
+ // If no duration can be calculated, let's output what the user provided.
+ if ( empty( $duration ) ) {
+ $duration = $atts['time'];
+ }
+
+ $html .= sprintf(
+ '<li class="jetpack-recipe-time">
+ <time itemprop="totalTime" datetime="%3$s"><strong>%1$s: </strong>%2$s</time>
+ </li>',
+ esc_html_x( 'Time', 'recipe', 'jetpack' ),
+ esc_html( $atts['time'] ),
+ esc_attr( $duration )
+ );
+ }
+
+ if ( '' !== $atts['difficulty'] ) {
+ $html .= sprintf(
+ '<li class="jetpack-recipe-difficulty"><strong>%1$s: </strong>%2$s</li>',
+ esc_html_x( 'Difficulty', 'recipe', 'jetpack' ),
+ esc_html( $atts['difficulty'] )
+ );
+ }
+
+ if ( '' !== $atts['source'] ) {
+ $html .= sprintf(
+ '<li class="jetpack-recipe-source"><strong>%1$s: </strong>',
+ esc_html_x( 'Source', 'recipe', 'jetpack' )
+ );
+
+ if ( '' !== $atts['sourceurl'] ) :
+ // Show the link if we have one.
+ $html .= sprintf(
+ '<a href="%2$s">%1$s</a>',
+ esc_html( $atts['source'] ),
+ esc_url( $atts['sourceurl'] )
+ );
+ else :
+ // Skip the link.
+ $html .= sprintf(
+ '%1$s',
+ esc_html( $atts['source'] )
+ );
+ endif;
+
+ $html .= '</li>';
+ }
+
+ if ( 'false' !== $atts['print'] ) {
+ $html .= sprintf(
+ '<li class="jetpack-recipe-print"><a href="#">%1$s</a></li>',
+ esc_html_x( 'Print', 'recipe', 'jetpack' )
+ );
+ }
+
+ $html .= '</ul>';
+ } // End if().
+
+ // Output the image, if we have one.
+ if ( '' !== $atts['image'] ) {
+ $html .= sprintf(
+ '<img class="jetpack-recipe-image" itemprop="image" src="%1$s" />',
+ esc_url( $atts['image'] )
+ );
+ }
+
+ // Output the description, if we have one.
+ if ( '' !== $atts['description'] ) {
+ $html .= sprintf(
+ '<p class="jetpack-recipe-description" itemprop="description">%1$s</p>',
+ esc_html( $atts['description'] )
+ );
+ }
+
+ // Print content between codes.
+ $html .= '<div class="jetpack-recipe-content">' . do_shortcode( $content ) . '</div>';
+
+ // Close it up.
+ $html .= '</div>';
+
+ // If there is a recipe within a recipe, remove the shortcode.
+ if ( has_shortcode( $html, 'recipe' ) ) {
+ remove_shortcode( 'recipe' );
+ }
+
+ // Sanitize html.
+ $html = wp_kses_post( $html );
+
+ // Return the HTML block.
+ return $html;
+ }
+
+ /**
+ * Our [recipe-notes] shortcode.
+ * Outputs ingredients, styled in a div.
+ *
+ * @param array $atts Array of shortcode attributes.
+ * @param string $content Post content.
+ *
+ * @return string HTML for recipe notes shortcode.
+ */
+ static function recipe_notes_shortcode( $atts, $content = '' ) {
+ $atts = shortcode_atts(
+ array(
+ 'title' => '', // string.
+ ),
+ $atts,
+ 'recipe-notes'
+ );
+
+ $html = '';
+
+ // Print a title if one exists.
+ if ( '' !== $atts['title'] ) {
+ $html .= '<h4 class="jetpack-recipe-notes-title">' . esc_html( $atts['title'] ) . '</h4>';
+ }
+
+ $html .= '<div class="jetpack-recipe-notes">';
+
+ // Format content using list functionality, if desired.
+ $html .= self::output_list_content( $content, 'notes' );
+
+ $html .= '</div>';
+
+ // Sanitize html.
+ $html = wp_kses_post( $html );
+
+ // Return the HTML block.
+ return $html;
+ }
+
+ /**
+ * Our [recipe-ingredients] shortcode.
+ * Outputs notes, styled in a div.
+ *
+ * @param array $atts Array of shortcode attributes.
+ * @param string $content Post content.
+ *
+ * @return string HTML for recipe ingredients shortcode.
+ */
+ static function recipe_ingredients_shortcode( $atts, $content = '' ) {
+ $atts = shortcode_atts(
+ array(
+ 'title' => esc_html_x( 'Ingredients', 'recipe', 'jetpack' ), // string.
+ ),
+ $atts,
+ 'recipe-ingredients'
+ );
+
+ $html = '<div class="jetpack-recipe-ingredients">';
+
+ // Print a title unless the user has opted to exclude it.
+ if ( 'false' !== $atts['title'] ) {
+ $html .= '<h4 class="jetpack-recipe-ingredients-title">' . esc_html( $atts['title'] ) . '</h4>';
+ }
+
+ // Format content using list functionality.
+ $html .= self::output_list_content( $content, 'ingredients' );
+
+ $html .= '</div>';
+
+ // Sanitize html.
+ $html = wp_kses_post( $html );
+
+ // Return the HTML block.
+ return $html;
+ }
+
+ /**
+ * Reusable function to check for shortened formatting.
+ * Basically, users can create lists with the following shorthand:
+ * - item one
+ * - item two
+ * - item three
+ * And we'll magically convert it to a list. This has the added benefit
+ * of including itemprops for the recipe schema.
+ *
+ * @param string $content HTML content.
+ * @param string $type Type of list.
+ *
+ * @return string content formatted as a list item
+ */
+ static function output_list_content( $content, $type ) {
+ $html = '';
+
+ switch ( $type ) {
+ case 'directions':
+ $list_item_replacement = '<li class="jetpack-recipe-directions">${1}</li>';
+ $itemprop = ' itemprop="recipeInstructions"';
+ $listtype = 'ol';
+ break;
+ case 'ingredients':
+ $list_item_replacement = '<li class="jetpack-recipe-ingredient" itemprop="recipeIngredient">${1}</li>';
+ $itemprop = '';
+ $listtype = 'ul';
+ break;
+ default:
+ $list_item_replacement = '<li class="jetpack-recipe-notes">${1}</li>';
+ $itemprop = '';
+ $listtype = 'ul';
+ }
+
+ // Check to see if the user is trying to use shortened formatting.
+ if (
+ strpos( $content, '&#8211;' ) !== false ||
+ strpos( $content, '&#8212;' ) !== false ||
+ strpos( $content, '-' ) !== false ||
+ strpos( $content, '*' ) !== false ||
+ strpos( $content, '#' ) !== false ||
+ strpos( $content, '–' ) !== false || // ndash.
+ strpos( $content, '—' ) !== false || // mdash.
+ preg_match( '/\d+\.\s/', $content )
+ ) {
+ // Remove breaks and extra whitespace.
+ $content = str_replace( "<br />\n", "\n", $content );
+ $content = trim( $content );
+
+ $ul_pattern = '/(?:^|\n|\<p\>)+(?:[\-–—]+|\&#8211;|\&#8212;|\*)+\h+(.*)/mi';
+ $ol_pattern = '/(?:^|\n|\<p\>)+(?:\d+\.|#+)+\h+(.*)/mi';
+
+ preg_match_all( $ul_pattern, $content, $ul_matches );
+ preg_match_all( $ol_pattern, $content, $ol_matches );
+
+ if ( 0 !== count( $ul_matches[0] ) || 0 !== count( $ol_matches[0] ) ) {
+
+ if ( 0 !== count( $ol_matches[0] ) ) {
+ $listtype = 'ol';
+ $list_item_pattern = $ol_pattern;
+ } else {
+ $listtype = 'ul';
+ $list_item_pattern = $ul_pattern;
+ }
+ $html .= '<' . $listtype . $itemprop . '>';
+ $html .= preg_replace( $list_item_pattern, $list_item_replacement, $content );
+ $html .= '</' . $listtype . '>';
+
+ // Strip out any empty <p> tags and stray </p> tags, because those are just silly.
+ $empty_p_pattern = '/(<p>)*\s*<\/p>/mi';
+ $html = preg_replace( $empty_p_pattern, '', $html );
+ } else {
+ $html .= do_shortcode( $content );
+ }
+ } else {
+ $html .= do_shortcode( $content );
+ }
+
+ // Return our formatted content.
+ return $html;
+ }
+
+ /**
+ * Our [recipe-directions] shortcode.
+ * Outputs directions, styled in a div.
+ *
+ * @param array $atts Array of shortcode attributes.
+ * @param string $content Post content.
+ *
+ * @return string HTML for recipe directions shortcode.
+ */
+ static function recipe_directions_shortcode( $atts, $content = '' ) {
+ $atts = shortcode_atts(
+ array(
+ 'title' => esc_html_x( 'Directions', 'recipe', 'jetpack' ), // string.
+ ),
+ $atts,
+ 'recipe-directions'
+ );
+
+ $html = '<div class="jetpack-recipe-directions">';
+
+ // Print a title unless the user has specified to exclude it.
+ if ( 'false' !== $atts['title'] ) {
+ $html .= '<h4 class="jetpack-recipe-directions-title">' . esc_html( $atts['title'] ) . '</h4>';
+ }
+
+ // Format content using list functionality.
+ $html .= self::output_list_content( $content, 'directions' );
+
+ $html .= '</div>';
+
+ // Sanitize html.
+ $html = wp_kses_post( $html );
+
+ // Return the HTML block.
+ return $html;
+ }
+
+ /**
+ * Use $themecolors array to style the Recipes shortcode
+ *
+ * @print style block
+ * @return string $style
+ */
+ function themecolor_styles() {
+ global $themecolors;
+ $style = '';
+
+ if ( isset( $themecolors ) ) {
+ $style .= '.jetpack-recipe { border-color: #' . esc_attr( $themecolors['border'] ) . '; }';
+ $style .= '.jetpack-recipe-title { border-bottom-color: #' . esc_attr( $themecolors['link'] ) . '; }';
+ }
+
+ return $style;
+ }
+
+}
+
+new Jetpack_Recipes();
diff --git a/plugins/jetpack/modules/shortcodes/scribd.php b/plugins/jetpack/modules/shortcodes/scribd.php
new file mode 100644
index 00000000..1810c2fa
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/scribd.php
@@ -0,0 +1,62 @@
+<?php
+
+/*
+ Scribd Short Code
+Author: Nick Momrik
+
+[scribd id=DOCUMENT_ID key=DOCUMENT_KEY mode=MODE]
+DOCUMENT_ID is an integer (also used as an object_id)
+DOCUMENT_KEY is an alphanumeric hash ('-' character as well)
+MODE can be 'list', 'book', 'slide', 'slideshow', or 'tile'
+
+[scribd id=39027960 key=key-3kaiwcjqhtipf25m8tw mode=list]
+*/
+
+function scribd_shortcode_handler( $atts ) {
+ $atts = shortcode_atts(
+ array(
+ 'id' => 0,
+ 'key' => 0,
+ 'mode' => '',
+ ),
+ $atts,
+ 'scribd'
+ );
+
+ $modes = array( 'list', 'book', 'slide', 'slideshow', 'tile' );
+
+ $atts['id'] = (int) $atts['id'];
+ if ( preg_match( '/^[A-Za-z0-9-]+$/', $atts['key'], $m ) ) {
+ $atts['key'] = $m[0];
+
+ if ( ! in_array( $atts['mode'], $modes ) ) {
+ $atts['mode'] = '';
+ }
+
+ return scribd_shortcode_markup( $atts );
+ } else {
+ return '';
+ }
+}
+
+function scribd_shortcode_markup( $atts ) {
+ $markup = <<<EOD
+<iframe class="scribd_iframe_embed" src="//www.scribd.com/embeds/$atts[id]/content?start_page=1&view_mode=$atts[mode]&access_key=$atts[key]" data-auto-height="true" scrolling="no" id="scribd_$atts[id]" width="100%" height="500" frameborder="0"></iframe>
+<div style="font-size:10px;text-align:center;width:100%"><a href="http://www.scribd.com/doc/$atts[id]" target="_blank">View this document on Scribd</a></div>
+EOD;
+
+ return $markup;
+}
+
+add_shortcode( 'scribd', 'scribd_shortcode_handler' );
+
+// Scribd supports HTTPS, so use that endpoint to get HTTPS-compatible embeds
+function scribd_https_oembed( $providers ) {
+ if ( isset( $providers['#https?://(www\.)?scribd\.com/doc/.*#i'] ) ) {
+ $providers['#https?://(www\.)?scribd\.com/doc/.*#i'][0] = 'https://www.scribd.com/services/oembed';
+ }
+
+ return $providers;
+}
+
+add_filter( 'oembed_providers', 'scribd_https_oembed' );
diff --git a/plugins/jetpack/modules/shortcodes/sitemap.php b/plugins/jetpack/modules/shortcodes/sitemap.php
new file mode 100644
index 00000000..d26210f0
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/sitemap.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Sitemap shortcode.
+ *
+ * Usage: [sitemap]
+ *
+ * @package Jetpack
+ */
+
+add_shortcode( 'sitemap', 'jetpack_sitemap_shortcode' );
+
+/**
+ * Renders a tree of pages.
+ *
+ * @since 4.5.0
+ *
+ * @return string
+ */
+function jetpack_sitemap_shortcode() {
+ $tree = wp_list_pages(
+ array(
+ 'title_li' => '<b><a href="/">' . esc_html( get_bloginfo( 'name' ) ) . '</a></b>',
+ 'exclude' => get_option( 'page_on_front' ),
+ 'echo' => false,
+ )
+ );
+ return empty( $tree )
+ ? ''
+ : '<ul class="jetpack-sitemap-shortcode">' . $tree . '</ul>';
+}
diff --git a/plugins/jetpack/modules/shortcodes/slideshare.php b/plugins/jetpack/modules/shortcodes/slideshare.php
new file mode 100644
index 00000000..898ae57e
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/slideshare.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * Slideshare shortcode format:
+ * Old style (still compatible): [slideshare id=5342235&doc=camprock-101002163655-phpapp01&w=300&h=200]
+ * New style: [slideshare id=5342235&w=300&h=200&fb=0&mw=0&mh=0&sc=no]
+ *
+ * Legend:
+ * id = Document ID provided by Slideshare
+ * w = Width of iFrame (int)
+ * h = Height of iFrame (int)
+ * fb = iFrame frameborder (int)
+ * mw = iFrame marginwidth (int)
+ * mh = iFrame marginheight (int)
+ * sc = iFrame Scrollbar (yes/no)
+ * pro = Slideshare Pro (yes/no)
+ * style = Inline CSS (string)
+ **/
+
+add_shortcode( 'slideshare', 'slideshare_shortcode' );
+
+function slideshare_shortcode( $atts ) {
+ global $content_width;
+
+ $params = shortcode_new_to_old_params( $atts );
+ parse_str( $params, $arguments );
+
+ if ( empty( $arguments ) ) {
+ return '<!-- SlideShare error: no arguments -->';
+ }
+
+ $attr = shortcode_atts(
+ array(
+ 'id' => '',
+ 'w' => '',
+ 'h' => '',
+ 'fb' => '',
+ 'mw' => '',
+ 'mh' => '',
+ 'sc' => '',
+ 'pro' => '',
+ 'style' => '',
+ ),
+ $arguments
+ );
+
+ // check that the Slideshare ID contains letters, numbers and query strings
+ $pattern = '/[^-_a-zA-Z0-9?=&]/';
+ if ( empty( $attr['id'] ) || preg_match( $pattern, $attr['id'] ) ) {
+ return '<!-- SlideShare error: id is missing or has illegal characters -->';
+ }
+
+ // check the width/height
+ $w = $attr['w'];
+ if ( empty( $w ) && ! empty( $content_width ) ) {
+ $w = intval( $content_width );
+ } elseif ( ! ( $w = intval( $w ) ) || $w < 300 || $w > 1600 ) {
+ $w = 425;
+ } else {
+ $w = intval( $w );
+ }
+
+ $h = ceil( $w * 348 / 425 ); // Note: user-supplied height is ignored.
+
+ if ( isset( $attr['pro'] ) && $attr['pro'] ) {
+ $source = 'https://www.slideshare.net/slidesharepro/' . $attr['id'];
+ } else {
+ $source = 'https://www.slideshare.net/slideshow/embed_code/' . $attr['id'];
+ }
+
+ if ( isset( $rel ) ) {
+ $source = add_query_arg( 'rel', intval( $rel ), $source );
+ }
+
+ if ( isset( $startSlide ) ) {
+ $source = add_query_arg( 'startSlide', intval( $startSlide ), $source );
+ }
+
+ $player = sprintf( "<iframe src='%s' width='%d' height='%d'", esc_url( $source ), $w, $h );
+
+ // check the frameborder
+ if ( ! empty( $attr['fb'] ) || '0' === $attr['fb'] ) {
+ $player .= " frameborder='" . intval( $attr['fb'] ) . "'";
+ }
+
+ // check the margin width; if not empty, cast as int
+ if ( ! empty( $attr['mw'] ) || '0' === $attr['mw'] ) {
+ $player .= " marginwidth='" . intval( $attr['mw'] ) . "'";
+ }
+
+ // check the margin height, if not empty, cast as int
+ if ( ! empty( $attr['mh'] ) || '0' === $attr['mh'] ) {
+ $player .= " marginheight='" . intval( $attr['mh'] ) . "'";
+ }
+
+ if ( ! empty( $attr['style'] ) ) {
+ $player .= " style='" . esc_attr( $attr['style'] ) . "'";
+ }
+
+ // check the scrollbar; cast as a lowercase string for comparison
+ if ( ! empty( $attr['sc'] ) ) {
+ $sc = strtolower( $attr['sc'] );
+
+ if ( in_array( $sc, array( 'yes', 'no' ) ) ) {
+ $player .= " scrolling='" . $sc . "'";
+ }
+ }
+
+ $player .= ' allowfullscreen webkitallowfullscreen mozallowfullscreen></iframe>';
+
+ /**
+ * Filter the returned SlideShare shortcode.
+ *
+ * @module shortcodes
+ *
+ * @since 4.7.0
+ *
+ * @param string $player The iframe to return.
+ * @param array $atts The attributes specified in the shortcode.
+ */
+ return apply_filters( 'jetpack_slideshare_shortcode', $player, $atts );
+}
diff --git a/plugins/jetpack/modules/shortcodes/slideshow.php b/plugins/jetpack/modules/shortcodes/slideshow.php
new file mode 100644
index 00000000..43428dc8
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/slideshow.php
@@ -0,0 +1,316 @@
+<?php
+
+/**
+ * Slideshow shortcode usage: [gallery type="slideshow"] or the older [slideshow]
+ */
+class Jetpack_Slideshow_Shortcode {
+ public $instance_count = 0;
+
+ function __construct() {
+ global $shortcode_tags;
+
+ // Only if the slideshow shortcode has not already been defined.
+ if ( ! array_key_exists( 'slideshow', $shortcode_tags ) ) {
+ add_shortcode( 'slideshow', array( $this, 'shortcode_callback' ) );
+ }
+
+ // Only if the gallery shortcode has not been redefined.
+ if ( isset( $shortcode_tags['gallery'] ) && 'gallery_shortcode' === $shortcode_tags['gallery'] ) {
+ add_filter( 'post_gallery', array( $this, 'post_gallery' ), 1002, 2 );
+ add_filter( 'jetpack_gallery_types', array( $this, 'add_gallery_type' ), 10 );
+ }
+
+ /**
+ * For the moment, comment out the setting for v2.8.
+ * The remainder should work as it always has.
+ * See: https://github.com/Automattic/jetpack/pull/85/files
+ */
+ // add_action( 'admin_init', array( $this, 'register_settings' ), 5 );
+ }
+
+ /**
+ * Responds to the [gallery] shortcode, but not an actual shortcode callback.
+ *
+ * @param $value string An empty string if nothing has modified the gallery output, the output html otherwise
+ * @param $attr array The shortcode attributes array
+ *
+ * @return string The (un)modified $value
+ */
+ function post_gallery( $value, $attr ) {
+ // Bail if somebody else has done something
+ if ( ! empty( $value ) ) {
+ return $value;
+ }
+
+ // If [gallery type="slideshow"] have it behave just like [slideshow]
+ if ( ! empty( $attr['type'] ) && 'slideshow' == $attr['type'] ) {
+ return $this->shortcode_callback( $attr );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Add the Slideshow type to gallery settings
+ *
+ * @see Jetpack_Tiled_Gallery::media_ui_print_templates
+ *
+ * @param $types array An array of types where the key is the value, and the value is the caption.
+ *
+ * @return array
+ */
+ function add_gallery_type( $types = array() ) {
+ $types['slideshow'] = esc_html__( 'Slideshow', 'jetpack' );
+
+ return $types;
+ }
+
+ function register_settings() {
+ add_settings_section( 'slideshow_section', __( 'Image Gallery Slideshow', 'jetpack' ), '__return_empty_string', 'media' );
+
+ add_settings_field( 'jetpack_slideshow_background_color', __( 'Background color', 'jetpack' ), array( $this, 'slideshow_background_color_callback' ), 'media', 'slideshow_section' );
+
+ register_setting( 'media', 'jetpack_slideshow_background_color', array( $this, 'slideshow_background_color_sanitize' ) );
+ }
+
+ function slideshow_background_color_callback() {
+ $options = array(
+ 'black' => __( 'Black', 'jetpack' ),
+ 'white' => __( 'White', 'jetpack' ),
+ );
+ $this->settings_select( 'jetpack_slideshow_background_color', $options );
+ }
+
+ function settings_select( $name, $values, $extra_text = '' ) {
+ if ( empty( $name ) || empty( $values ) || ! is_array( $values ) ) {
+ return;
+ }
+ $option = get_option( $name );
+ ?>
+ <fieldset>
+ <select name="<?php echo esc_attr( $name ); ?>" id="<?php echo esc_attr( $name ); ?>">
+ <?php foreach ( $values as $key => $value ) : ?>
+ <option value="<?php echo esc_attr( $key ); ?>" <?php selected( $key, $option ); ?>>
+ <?php echo esc_html( $value ); ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ <?php if ( ! empty( $extra_text ) ) : ?>
+ <p class="description"><?php echo esc_html( $extra_text ); ?></p>
+ <?php endif; ?>
+ </fieldset>
+ <?php
+ }
+
+ function slideshow_background_color_sanitize( $value ) {
+ return ( 'white' == $value ) ? 'white' : 'black';
+ }
+
+ function shortcode_callback( $attr ) {
+ $post_id = get_the_ID();
+
+ $attr = shortcode_atts(
+ array(
+ 'trans' => 'fade',
+ 'order' => 'ASC',
+ 'orderby' => 'menu_order ID',
+ 'id' => $post_id,
+ 'include' => '',
+ 'exclude' => '',
+ 'autostart' => true,
+ 'size' => '',
+ ),
+ $attr,
+ 'slideshow'
+ );
+
+ if ( 'rand' == strtolower( $attr['order'] ) ) {
+ $attr['orderby'] = 'none';
+ }
+
+ $attr['orderby'] = sanitize_sql_orderby( $attr['orderby'] );
+ if ( ! $attr['orderby'] ) {
+ $attr['orderby'] = 'menu_order ID';
+ }
+
+ if ( ! $attr['size'] ) {
+ $attr['size'] = 'full';
+ }
+
+ // Don't restrict to the current post if include
+ $post_parent = ( empty( $attr['include'] ) ) ? intval( $attr['id'] ) : null;
+
+ $attachments = get_posts(
+ array(
+ 'post_status' => 'inherit',
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'image',
+ 'posts_per_page' => - 1,
+ 'post_parent' => $post_parent,
+ 'order' => $attr['order'],
+ 'orderby' => $attr['orderby'],
+ 'include' => $attr['include'],
+ 'exclude' => $attr['exclude'],
+ 'suppress_filters' => false,
+ )
+ );
+
+ if ( count( $attachments ) < 1 ) {
+ return false;
+ }
+
+ $gallery_instance = sprintf( 'gallery-%d-%d', $attr['id'], ++$this->instance_count );
+
+ $gallery = array();
+ foreach ( $attachments as $attachment ) {
+ $attachment_image_src = wp_get_attachment_image_src( $attachment->ID, $attr['size'] );
+ $attachment_image_src = $attachment_image_src[0]; // [url, width, height]
+ $attachment_image_title = get_the_title( $attachment->ID );
+ $attachment_image_alt = get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true );
+ /**
+ * Filters the Slideshow slide caption.
+ *
+ * @module shortcodes
+ *
+ * @since 2.3.0
+ *
+ * @param string wptexturize( strip_tags( $attachment->post_excerpt ) ) Post excerpt.
+ * @param string $attachment ->ID Attachment ID.
+ */
+ $caption = apply_filters( 'jetpack_slideshow_slide_caption', wptexturize( strip_tags( $attachment->post_excerpt ) ), $attachment->ID );
+
+ $gallery[] = (object) array(
+ 'src' => (string) esc_url_raw( $attachment_image_src ),
+ 'id' => (string) $attachment->ID,
+ 'title' => (string) esc_attr( $attachment_image_title ),
+ 'alt' => (string) esc_attr( $attachment_image_alt ),
+ 'caption' => (string) $caption,
+ 'itemprop' => 'image',
+ );
+ }
+
+ $color = Jetpack_Options::get_option( 'slideshow_background_color', 'black' );
+
+ $js_attr = array(
+ 'gallery' => $gallery,
+ 'selector' => $gallery_instance,
+ 'trans' => $attr['trans'] ? $attr['trans'] : 'fade',
+ 'autostart' => $attr['autostart'] ? $attr['autostart'] : 'true',
+ 'color' => $color,
+ );
+
+ // Show a link to the gallery in feeds.
+ if ( is_feed() ) {
+ return sprintf(
+ '<a href="%s">%s</a>',
+ esc_url( get_permalink( $post_id ) . '#' . $gallery_instance . '-slideshow' ),
+ esc_html__( 'Click to view slideshow.', 'jetpack' )
+ );
+ }
+
+ return $this->slideshow_js( $js_attr );
+ }
+
+ /**
+ * Render the slideshow js
+ *
+ * Returns the necessary markup and js to fire a slideshow.
+ *
+ * @param $attr array Attributes for the slideshow.
+ *
+ * @uses $this->enqueue_scripts()
+ *
+ * @return string HTML output.
+ */
+ function slideshow_js( $attr ) {
+ // Enqueue scripts
+ $this->enqueue_scripts();
+
+ $output = '';
+
+ if ( defined( 'JSON_HEX_AMP' ) ) {
+ // This is nice to have, but not strictly necessary since we use _wp_specialchars() below
+ $gallery = json_encode( $attr['gallery'], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); // phpcs:ignore PHPCompatibility
+ } else {
+ $gallery = json_encode( $attr['gallery'] );
+ }
+
+ $output .= '<p class="jetpack-slideshow-noscript robots-nocontent">' . esc_html__( 'This slideshow requires JavaScript.', 'jetpack' ) . '</p>';
+ $output .= sprintf(
+ '<div id="%s" class="slideshow-window jetpack-slideshow slideshow-%s" data-trans="%s" data-autostart="%s" data-gallery="%s" itemscope itemtype="https://schema.org/ImageGallery"></div>',
+ esc_attr( $attr['selector'] . '-slideshow' ),
+ esc_attr( $attr['color'] ),
+ esc_attr( $attr['trans'] ),
+ esc_attr( $attr['autostart'] ),
+ /*
+ * The input to json_encode() above can contain '&quot;'.
+ *
+ * For calls to json_encode() lacking the JSON_HEX_AMP option,
+ * that '&quot;' is left unaltered. Running '&quot;' through esc_attr()
+ * also leaves it unaltered since esc_attr() does not double-encode.
+ *
+ * This means we end up with an attribute like
+ * `data-gallery="{&quot;foo&quot;:&quot;&quot;&quot;}"`,
+ * which is interpreted by the browser as `{"foo":"""}`,
+ * which cannot be JSON decoded.
+ *
+ * The preferred workaround is to include the JSON_HEX_AMP (and friends)
+ * options, but these are not available until 5.3.0.
+ * Alternatively, we can use _wp_specialchars( , , , true ) instead of
+ * esc_attr(), which will double-encode.
+ *
+ * Since we can't rely on JSON_HEX_AMP, we do both.
+ */
+ _wp_specialchars( wp_check_invalid_utf8( $gallery ), ENT_QUOTES, false, true )
+ );
+
+ return $output;
+ }
+
+ /**
+ * Actually enqueues the scripts and styles.
+ */
+ function enqueue_scripts() {
+
+ wp_enqueue_script( 'jquery-cycle', plugins_url( '/js/jquery.cycle.min.js', __FILE__ ), array( 'jquery' ), '20161231', true );
+ wp_enqueue_script(
+ 'jetpack-slideshow',
+ Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/slideshow-shortcode.min.js', 'modules/shortcodes/js/slideshow-shortcode.js' ),
+ array( 'jquery-cycle' ),
+ '20160119.1',
+ true
+ );
+ wp_enqueue_style( 'jetpack-slideshow', plugins_url( '/css/slideshow-shortcode.css', __FILE__ ) );
+ wp_style_add_data( 'jetpack-slideshow', 'rtl', 'replace' );
+
+ wp_localize_script(
+ 'jetpack-slideshow',
+ 'jetpackSlideshowSettings',
+ /**
+ * Filters the slideshow JavaScript spinner.
+ *
+ * @module shortcodes
+ *
+ * @since 2.1.0
+ * @since 4.7.0 Added the `speed` option to the array of options.
+ *
+ * @param array $args
+ * - string - spinner - URL of the spinner image.
+ * - string - speed - Speed of the slideshow. Defaults to 4000.
+ */
+ apply_filters(
+ 'jetpack_js_slideshow_settings',
+ array(
+ 'spinner' => plugins_url( '/img/slideshow-loader.gif', __FILE__ ),
+ 'speed' => '4000',
+ )
+ )
+ );
+ }
+
+ public static function init() {
+ new Jetpack_Slideshow_Shortcode();
+ }
+}
+
+Jetpack_Slideshow_Shortcode::init();
diff --git a/plugins/jetpack/modules/shortcodes/soundcloud.php b/plugins/jetpack/modules/shortcodes/soundcloud.php
new file mode 100644
index 00000000..71f281c3
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/soundcloud.php
@@ -0,0 +1,327 @@
+<?php
+/*
+Plugin Name: SoundCloud Shortcode
+Plugin URI: https://wordpress.org/extend/plugins/soundcloud-shortcode/
+Description: Converts SoundCloud WordPress shortcodes to a SoundCloud widget. Example: [soundcloud]http://soundcloud.com/forss/flickermood[/soundcloud]
+Version: 2.3
+Author: SoundCloud Inc., simplified for Jetpack by Automattic, Inc.
+Author URI: http://soundcloud.com
+License: GPLv2
+
+Original version: Johannes Wagener <johannes@soundcloud.com>
+Options support: Tiffany Conroy <tiffany@soundcloud.com>
+HTML5 & oEmbed support: Tim Bormans <tim@soundcloud.com>
+*/
+
+/*
+A8C: Taken from http://plugins.svn.wordpress.org/soundcloud-shortcode/trunk/
+at revision 664386.
+
+Commenting out (instead of removing) and replacing code with custom modifs
+so it's eqsy to see what differs from the standard DOTORG version.
+
+All custom modifs are annoted with "A8C" keyword in comment.
+*/
+
+/**
+ * Register oEmbed provider
+ */
+
+/*
+ A8C: oEmbed is handled now in core; see wp-includes/class-oembed.php
+wp_oembed_add_provider( '#https?://(?:api\.)?soundcloud\.com/.*#i', 'http://soundcloud.com/oembed', true );
+*/
+
+
+/**
+ * Register SoundCloud shortcode
+ */
+
+add_shortcode( 'soundcloud', 'soundcloud_shortcode' );
+
+/**
+ * SoundCloud shortcode handler
+ *
+ * @param string|array $atts The attributes passed to the shortcode like [soundcloud attr1="value" /].
+ * Is an empty string when no arguments are given.
+ * @param string $content The content between non-self closing [soundcloud]...[/soundcloud] tags.
+ *
+ * @return string Widget embed code HTML
+ */
+function soundcloud_shortcode( $atts, $content = null ) {
+
+ // Custom shortcode options
+ $shortcode_options = array_merge( array( 'url' => trim( $content ) ), is_array( $atts ) ? $atts : array() );
+
+ // Turn shortcode option "param" (param=value&param2=value) into array
+ $shortcode_params = array();
+ if ( isset( $shortcode_options['params'] ) ) {
+ parse_str( html_entity_decode( $shortcode_options['params'] ), $shortcode_params );
+ }
+ $shortcode_options['params'] = $shortcode_params;
+
+ /*
+ A8C: The original plugin exposes options we don't. SoundCloud omits "visual" shortcode
+ option when false, so if logic here remains, impossible to have non-visual shortcode.
+ $player_type = soundcloud_get_option( 'player_type', 'visual' );
+ $isIframe = $player_type !== 'flash';
+ $isVisual = ! $player_type || $player_type === 'visual' || $shortcode_options['visual'];
+ */
+
+ // User preference options
+ $plugin_options = array_filter(
+ array(
+ 'iframe' => true, // A8C: See above comment; flash is not a supported option
+ 'width' => soundcloud_get_option( 'player_width' ),
+ 'height' => soundcloud_url_has_tracklist( $shortcode_options['url'] ) ? soundcloud_get_option( 'player_height_multi' ) : soundcloud_get_option( 'player_height' ),
+ 'params' => array_filter(
+ array(
+ 'auto_play' => soundcloud_get_option( 'auto_play' ),
+ 'show_comments' => soundcloud_get_option( 'show_comments' ),
+ 'color' => soundcloud_get_option( 'color' ),
+ 'visual' => 'false', // A8C: Merged with params below at $options assignment
+ )
+ ),
+ )
+ );
+
+ // Needs to be an array
+ if ( ! isset( $plugin_options['params'] ) ) {
+ $plugin_options['params'] = array();
+ }
+
+ // plugin options < shortcode options
+ $options = array_merge(
+ $plugin_options,
+ $shortcode_options
+ );
+
+ // plugin params < shortcode params
+ $options['params'] = array_merge(
+ $plugin_options['params'],
+ $shortcode_options['params']
+ );
+
+ // The "url" option is required
+ if ( ! isset( $options['url'] ) ) {
+ return '';
+ } else {
+ $options['url'] = trim( $options['url'] );
+ }
+
+ // Both "width" and "height" need to be integers
+ if ( isset( $options['width'] ) && ! preg_match( '/^\d+$/', $options['width'] ) ) {
+ // set to 0 so oEmbed will use the default 100% and WordPress themes will leave it alone
+ $options['width'] = 0;
+ }
+ if ( isset( $options['height'] ) && ! preg_match( '/^\d+$/', $options['height'] ) ) {
+ unset( $options['height'] );
+ }
+
+ // The "iframe" option must be true to load the iframe widget
+ $iframe = soundcloud_booleanize( $options['iframe'] );
+
+ // Remove visual parameter from Flash widget, when it's false because that's the default, or when displaying the smallest player
+ if ( $options['params']['visual'] && ( ! $iframe || ! soundcloud_booleanize( $options['params']['visual'] ) || ( isset( $options['height'] ) && '20' == $options['height'] ) ) ) {
+ unset( $options['params']['visual'] );
+ }
+
+ // Merge in "url" value
+ $options['params'] = array_merge(
+ array(
+ 'url' => $options['url'],
+ ),
+ $options['params']
+ );
+
+ // Return html embed code
+ if ( $iframe ) {
+ return soundcloud_iframe_widget( $options );
+ } else {
+ return soundcloud_flash_widget( $options );
+ }
+}
+
+/**
+ * Plugin options getter
+ *
+ * @param string|array $option Option name
+ * @param mixed $default Default value
+ *
+ * @return mixed Option value
+ */
+function soundcloud_get_option( $option, $default = false ) {
+ $value = get_option( 'soundcloud_' . $option );
+
+ return $value === '' ? $default : $value;
+}
+
+/**
+ * Booleanize a value
+ *
+ * @param boolean|string $value
+ *
+ * @return boolean
+ */
+function soundcloud_booleanize( $value ) {
+ return is_bool( $value ) ? $value : $value === 'true' ? true : false;
+}
+
+/**
+ * Decide if a url has a tracklist
+ *
+ * @param string $url
+ *
+ * @return boolean
+ */
+function soundcloud_url_has_tracklist( $url ) {
+ return preg_match( '/^(.+?)\/(sets|groups|playlists)\/(.+?)$/', $url );
+}
+
+/**
+ * Parameterize url
+ *
+ * @param array $match Matched regex
+ *
+ * @return string Parameterized url
+ */
+function soundcloud_oembed_params_callback( $match ) {
+ global $soundcloud_oembed_params;
+
+ // Convert URL to array
+ $url = parse_url( urldecode( $match[1] ) );
+ // Convert URL query to array
+ parse_str( $url['query'], $query_array );
+ // Build new query string
+ $query = http_build_query( array_merge( $query_array, $soundcloud_oembed_params ) );
+
+ return 'src="' . $url['scheme'] . '://' . $url['host'] . $url['path'] . '?' . $query;
+}
+
+/**
+ * Iframe widget embed code
+ *
+ * @param array $options Parameters
+ *
+ * @return string Iframe embed code
+ */
+function soundcloud_iframe_widget( $options ) {
+
+ // Build URL
+ $url = set_url_scheme( 'https://w.soundcloud.com/player/?' . http_build_query( $options['params'] ) );
+ // Set default width if not defined
+ $width = isset( $options['width'] ) && $options['width'] !== 0 ? $options['width'] : '100%';
+ // Set default height if not defined
+ $height = isset( $options['height'] ) && $options['height'] !== 0
+ ? $options['height']
+ : ( soundcloud_url_has_tracklist( $options['url'] ) || ( isset( $options['params']['visual'] ) && soundcloud_booleanize( $options['params']['visual'] ) ) ? '450' : '166' );
+
+ return sprintf( '<iframe width="%s" height="%s" scrolling="no" frameborder="no" src="%s"></iframe>', $width, $height, $url );
+}
+
+/**
+ * Legacy Flash widget embed code
+ *
+ * @param array $options Parameters
+ *
+ * @return string Flash embed code
+ */
+function soundcloud_flash_widget( $options ) {
+ // Build URL
+ $url = set_url_scheme( 'https://player.soundcloud.com/player.swf?' . http_build_query( $options['params'] ) );
+ // Set default width if not defined
+ $width = isset( $options['width'] ) && $options['width'] !== 0 ? $options['width'] : '100%';
+ // Set default height if not defined
+ $height = isset( $options['height'] ) && $options['height'] !== 0 ? $options['height'] : ( soundcloud_url_has_tracklist( $options['url'] ) ? '255' : '81' );
+
+ return preg_replace(
+ '/\s\s+/',
+ '',
+ sprintf(
+ '<object width="%s" height="%s">
+ <param name="movie" value="%s" />
+ <param name="allowscriptaccess" value="always" />
+ <embed width="%s" height="%s" src="%s" allowscriptaccess="always" type="application/x-shockwave-flash"></embed>
+ </object>',
+ $width,
+ $height,
+ $url,
+ $width,
+ $height,
+ $url
+ )
+ );
+}
+
+/**
+ * SoundCloud Embed Reversal
+ *
+ * Converts a generic HTML embed code from SoundClound into a
+ * WordPress.com-compatibly shortcode.
+ *
+ * @param string $content HTML content.
+ *
+ * @return string Parsed content.
+ */
+function jetpack_soundcloud_embed_reversal( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, 'w.soundcloud.com/player' ) ) {
+ return $content;
+ }
+
+ /*
+ Sample embed code:
+
+ <iframe width="100%" height="450" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/150745932&amp;auto_play=false&amp;hide_related=false&amp;show_comments=true&amp;show_user=true&amp;show_reposts=false&amp;visual=true"></iframe>
+ */
+
+ $regexes = array();
+
+ $regexes[] = '#<iframe[^>]+?src="((?:https?:)?//w\.soundcloud\.com/player/[^"\']++)"[^>]*+>\s*?</iframe>#i';
+ $regexes[] = '#&lt;iframe(?:[^&]|&(?!gt;))+?src="((?:https?:)?//w\.soundcloud\.com/player/[^"\']++)"(?:[^&]|&(?!gt;))*+&gt;\s*?&lt;/iframe&gt;#i';
+
+ foreach ( $regexes as $regex ) {
+ if ( ! preg_match_all( $regex, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+
+ // if pasted from the visual editor - prevent double encoding
+ $match[1] = str_replace( '&amp;amp;', '&amp;', $match[1] );
+
+ $args = parse_url( html_entity_decode( $match[1] ), PHP_URL_QUERY );
+ $args = wp_parse_args( $args );
+
+ if ( ! preg_match( '#^(?:https?:)?//api\.soundcloud\.com/.+$#i', $args['url'], $url_matches ) ) {
+ continue;
+ }
+
+ if ( ! preg_match( '#height="(\d+)"#i', $match[0], $hmatch ) ) {
+ $height = '';
+ } else {
+ $height = ' height="' . intval( $hmatch[1] ) . '"';
+ }
+
+ unset( $args['url'] );
+ $params = 'params="';
+ if ( count( $args ) > 0 ) {
+ foreach ( $args as $key => $value ) {
+ $params .= esc_html( $key ) . '=' . esc_html( $value ) . '&amp;';
+ }
+ $params = substr( $params, 0, -5 );
+ }
+ $params .= '"';
+
+ $shortcode = '[soundcloud url="' . esc_url( $url_matches[0] ) . '" ' . $params . ' width="100%"' . $height . ' iframe="true" /]';
+
+ $replace_regex = sprintf( '#\s*%s\s*#', preg_quote( $match[0], '#' ) );
+ $content = preg_replace( $replace_regex, sprintf( "\n\n%s\n\n", $shortcode ), $content );
+ /** This action is documented in modules/shortcodes/youtube.php */
+ do_action( 'jetpack_embed_to_shortcode', 'soundcloud', $url_matches[0] );
+ }
+ }
+
+ return $content;
+}
+
+add_filter( 'pre_kses', 'jetpack_soundcloud_embed_reversal' );
diff --git a/plugins/jetpack/modules/shortcodes/spotify.php b/plugins/jetpack/modules/shortcodes/spotify.php
new file mode 100644
index 00000000..ff3c38b7
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/spotify.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * Spotify shortcode.
+ *
+ * Usage:
+ * [spotify id="spotify:track:4bz7uB4edifWKJXSDxwHcs" width="400" height="100"]
+ *
+ * @package Jetpack
+ */
+
+if ( ! shortcode_exists( 'spotify' ) ) {
+ add_shortcode( 'spotify', 'jetpack_spotify_shortcode' );
+
+ if ( get_option( 'embed_autourls' ) ) {
+ // If user enabled autourls, also convert syntax like spotify:track:4bz7uB4edifWKJXSDxwHcs.
+ add_filter( 'the_content', 'jetpack_spotify_embed_ids', 7 );
+ }
+}
+
+/**
+ * Parse shortcode arguments and render its output.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode attributes.
+ * @param string $content Post Content.
+ *
+ * @return string
+ */
+function jetpack_spotify_shortcode( $atts = array(), $content = '' ) {
+
+ if ( ! empty( $content ) ) {
+ $id = $content;
+ } elseif ( ! empty( $atts['id'] ) ) {
+ $id = $atts['id'];
+ } elseif ( ! empty( $atts[0] ) ) {
+ $id = $atts[0];
+ } else {
+ return '<!-- Missing Spotify ID -->';
+ }
+
+ if ( empty( $atts['width'] ) ) {
+ $atts['width'] = 300;
+ }
+
+ if ( empty( $atts['height'] ) ) {
+ $atts['height'] = 380;
+ }
+
+ $atts['width'] = (int) $atts['width'];
+ $atts['height'] = (int) $atts['height'];
+
+ // Spotify accepts both URLs and their Spotify ID format, so let them sort it out and validate.
+ $embed_url = add_query_arg( 'uri', rawurlencode( $id ), 'https://embed.spotify.com/' );
+
+ return '<iframe src="' . esc_url( $embed_url ) . '" style="display:block; margin:0 auto; width:' . esc_attr( $atts['width'] ) . 'px; height:' . esc_attr( $atts['height'] ) . 'px;" frameborder="0" allowtransparency="true"></iframe>';
+}
+
+/**
+ * Turn text like this on it's own line into an embed: spotify:track:4bz7uB4edifWKJXSDxwHcs
+ * The core WordPress embed functionality only works with URLs
+ * Modified version of WP_Embed::autoembed()
+ *
+ * @since 4.5.0
+ *
+ * @param string $content Post content.
+ *
+ * @return string
+ */
+function jetpack_spotify_embed_ids( $content ) {
+ $textarr = wp_html_split( $content );
+
+ foreach ( $textarr as &$element ) {
+ if ( '' === $element || '<' === $element[0] ) {
+ continue;
+ }
+
+ if ( substr( ltrim( $element ), 0, 8 ) !== 'spotify:' ) {
+ continue;
+ }
+
+ $element = preg_replace_callback( '|^\s*(spotify:[^\s"]+:[^\s"]+)\s*$|im', 'jetpack_spotify_embed_ids_callback', $element );
+ }
+
+ return implode( '', $textarr );
+}
+
+/**
+ * Call shortcode with ID provided by matching pattern.
+ *
+ * @since 4.5.0
+ *
+ * @param array $matches Array of matches for Spofify links.
+ *
+ * @return string
+ */
+function jetpack_spotify_embed_ids_callback( $matches ) {
+ return "\n" . jetpack_spotify_shortcode( array(), $matches[1] ) . "\n";
+}
diff --git a/plugins/jetpack/modules/shortcodes/ted.php b/plugins/jetpack/modules/shortcodes/ted.php
new file mode 100644
index 00000000..f66f77d5
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/ted.php
@@ -0,0 +1,78 @@
+<?php
+/*
+ * TED Player embed code
+ * http://www.ted.com
+ *
+ * http://www.ted.com/talks/view/id/210
+ * http://www.ted.com/talks/marc_goodman_a_vision_of_crimes_in_the_future.html
+ * [ted id="210" lang="en"]
+ * [ted id="http://www.ted.com/talks/view/id/210" lang="en"]
+ * [ted id=1539 lang=fr width=560 height=315]
+ */
+
+wp_oembed_add_provider( '!https?://(www\.)?ted.com/talks/view/id/.+!i', 'http://www.ted.com/talks/oembed.json', true );
+wp_oembed_add_provider( '!https?://(www\.)?ted.com/talks/[a-zA-Z\-\_]+\.html!i', 'http://www.ted.com/talks/oembed.json', true );
+
+function jetpack_shortcode_get_ted_id( $atts ) {
+ return ( ! empty( $atts['id'] ) ? $atts['id'] : 0 );
+}
+
+add_shortcode( 'ted', 'shortcode_ted' );
+function shortcode_ted( $atts ) {
+ global $wp_embed;
+
+ $defaults = array(
+ 'id' => '',
+ 'width' => '',
+ 'height' => '',
+ 'lang' => 'en',
+ );
+ $atts = shortcode_atts( $defaults, $atts, 'ted' );
+
+ if ( empty( $atts['id'] ) ) {
+ return '<!-- Missing TED ID -->';
+ }
+
+ $url = '';
+ if ( preg_match( '#^[\d]+$#', $atts['id'], $matches ) ) {
+ $url = 'http://ted.com/talks/view/id/' . $matches[0];
+ } elseif ( preg_match( '#^https?://(www\.)?ted\.com/talks/view/id/[0-9]+$#', $atts['id'], $matches ) ) {
+ $url = $matches[0];
+ }
+
+ unset( $atts['id'] );
+
+ $args = array();
+ if ( is_numeric( $atts['width'] ) ) {
+ $args['width'] = $atts['width'];
+ } elseif ( $embed_size_w = get_option( 'embed_size_w' ) ) {
+ $args['width'] = $embed_size_w;
+ } elseif ( ! empty( $GLOBALS['content_width'] ) ) {
+ $args['width'] = (int) $GLOBALS['content_width'];
+ } else {
+ $args['width'] = 500;
+ }
+
+ // Default to a 16x9 aspect ratio if there's no height set
+ if ( is_numeric( $atts['height'] ) ) {
+ $args['height'] = $atts['height'];
+ } else {
+ $args['height'] = $args['width'] * 0.5625;
+ }
+
+ if ( ! empty( $atts['lang'] ) ) {
+ $args['lang'] = sanitize_key( $atts['lang'] );
+ add_filter( 'oembed_fetch_url', 'ted_filter_oembed_fetch_url', 10, 3 );
+ }
+ $retval = $wp_embed->shortcode( $args, $url );
+ remove_filter( 'oembed_fetch_url', 'ted_filter_oembed_fetch_url', 10 );
+
+ return $retval;
+}
+
+/**
+ * Filter the request URL to also include the $lang parameter
+ */
+function ted_filter_oembed_fetch_url( $provider, $url, $args ) {
+ return add_query_arg( 'lang', $args['lang'], $provider );
+}
diff --git a/plugins/jetpack/modules/shortcodes/tweet.php b/plugins/jetpack/modules/shortcodes/tweet.php
new file mode 100644
index 00000000..5d6acd29
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/tweet.php
@@ -0,0 +1,276 @@
+<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Tweet shortcode.
+ * Params map to key value pairs, and all but tweet are optional:
+ * tweet = id or permalink url* (Required)
+ * align = none|left|right|center
+ * width = number in pixels example: width="300"
+ * lang = en|fr|de|ko|etc... language country code.
+ * hide_thread = true | false **
+ * hide_media = true | false **
+ *
+ * Basic:
+ * [tweet https://twitter.com/jack/statuses/20 width="350"]
+ *
+ * More parameters and another tweet syntax admitted:
+ * [tweet tweet="https://twitter.com/jack/statuses/20" align="left" width="350" align="center" lang="es"]
+ *
+ * @package Jetpack
+ */
+
+add_shortcode( 'tweet', array( 'Jetpack_Tweet', 'jetpack_tweet_shortcode' ) );
+
+/**
+ * Tweet Shortcode class.
+ */
+class Jetpack_Tweet {
+
+ /**
+ * Array of arguments about a tweet.
+ *
+ * @var array
+ */
+ public static $provider_args;
+
+ /**
+ * Parse shortcode arguments and render its output.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts Shortcode parameters.
+ *
+ * @return string
+ */
+ public static function jetpack_tweet_shortcode( $atts ) {
+ global $wp_embed;
+
+ $default_atts = array(
+ 'tweet' => '',
+ 'align' => 'none',
+ 'width' => '',
+ 'lang' => 'en',
+ 'hide_thread' => 'false',
+ 'hide_media' => 'false',
+ );
+
+ $attr = shortcode_atts( $default_atts, $atts );
+
+ self::$provider_args = $attr;
+
+ /*
+ * figure out the tweet id for the requested tweet
+ * supporting both omitted attributes and tweet="tweet_id"
+ * and supporting both an id and a URL
+ */
+ if ( empty( $attr['tweet'] ) && ! empty( $atts[0] ) ) {
+ $attr['tweet'] = $atts[0];
+ }
+
+ if ( ctype_digit( $attr['tweet'] ) ) {
+ $id = 'https://twitter.com/jetpack/status/' . $attr['tweet'];
+ $tweet_id = intval( $attr['tweet'] );
+ } else {
+ preg_match( '/^http(s|):\/\/twitter\.com(\/\#\!\/|\/)([a-zA-Z0-9_]{1,20})\/status(es)*\/(\d+)$/', $attr['tweet'], $urlbits );
+
+ if ( isset( $urlbits[5] ) && intval( $urlbits[5] ) ) {
+ $id = 'https://twitter.com/' . $urlbits[3] . '/status/' . intval( $urlbits[5] );
+ $tweet_id = intval( $urlbits[5] );
+ } else {
+ return '<!-- Invalid tweet id -->';
+ }
+ }
+
+ /*
+ * Fetch tweet.
+ *
+ * On WordPress.com, we also cache tweets for better performance and less requests.
+ */
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ /*
+ * See if we have the tweet stored in our tweet store
+ * if not get_tweet_store queues up a job to request
+ */
+ $data = get_tweet_store( $tweet_id );
+ if ( $data ) {
+ $tweet_handler = new Tweet_Handler();
+
+ /*
+ * Replace Unicode characters with ther entities like Blackbird Pie v 0.3.2 did
+ * to store tweets from other languages (important for non-english bloggers)
+ */
+ $data->text = $tweet_handler->unicode_replace_entities( $data->text );
+ $data->user->screen_name = $tweet_handler->unicode_replace_entities( $data->user->screen_name );
+ $data->user->name = $tweet_handler->unicode_replace_entities( $data->user->name );
+
+ $tweet = esc_html( $data->text );
+ $tweet = $tweet_handler->expand_tco_links( $tweet, $data );
+
+ $tweet = $tweet_handler->autolink( $tweet );
+
+ $screen_name = esc_html( $data->user->screen_name );
+ $name = esc_html( $data->user->name );
+
+ $url = 'https://twitter.com/' . $screen_name . '/status/' . intval( $data->id );
+
+ // Only show the user's real name if they set it to something different from their screename.
+ if ( $screen_name !== $name ) {
+ $real_name = '<br />' . $name;
+ } else {
+ $real_name = '<br />&nbsp;';
+ }
+
+ $time = strtotime( $data->created_at );
+ $human_readable = date( 'F d, Y', $time );
+ $data_datetime = date( 'Y-m-d\TH:i:sP', $time );
+
+ /*
+ * Additional params.
+ */
+
+ // align (float).
+ $extra_classes = '';
+ if ( in_array( $attr['align'], array( 'left', 'right', 'center' ), true ) ) {
+ $extra_classes = ' tw-align-' . $attr['align'];
+ }
+
+ if ( 'true' === $attr['hide_thread'] ) {
+ $extra_classes .= ' tw-hide-thread';
+ }
+
+ if ( 'true' === $attr['hide_media'] ) {
+ $extra_classes .= ' tw-hide-media';
+ }
+
+ // lang.
+ $lang = substr( $attr['lang'], 0, 2 );
+ if ( empty( $lang ) ) {
+ $lang = 'en';
+ }
+
+ // width.
+ $width_html = '';
+ $width = intval( $attr['width'] );
+ if ( $width > 100 ) {
+ $width_html = ' width="' . esc_attr( $width ) . '"';
+ }
+
+ // in reply to id (conversation tweets).
+ $in_reply_to_html = '';
+ $in_reply_to = intval( $data->in_reply_to_status_id );
+ if ( ! empty( $in_reply_to ) && 'false' === $attr['hide_thread'] ) {
+ $in_reply_to_html = ' data-in-reply-to="' . esc_attr( $in_reply_to ) . '"';
+ }
+
+ // Generate the HTML output.
+ $output = sprintf(
+ '<blockquote class="twitter-tweet%1$s"%2$s%3$s lang="%4$s"><p>%5$s</p>&mdash; %6$s (@%7$s) <a href="%8$s" data-datetime="%9$s">%10$s</a></blockquote>',
+ esc_attr( $extra_classes ),
+ $width_html,
+ $in_reply_to_html,
+ esc_attr( $lang ),
+ $tweet,
+ wp_kses( $real_name, array( 'br' => array() ) ),
+ esc_html( $screen_name ),
+ esc_url( $url ),
+ esc_attr( $data_datetime ),
+ esc_html( $human_readable )
+ );
+ } else {
+ /**
+ * Filter the default display when a tweet is not available in the store.
+ * Not available in Jetpack.
+ *
+ * @module shortcodes
+ *
+ * @since 5.1.0
+ *
+ * @param string $message Default display when a tweet is not available.
+ * @param string $id Twitter URL.
+ * @param array $attr Shortcode attributes.
+ */
+ return apply_filters( 'tweet_shortcode_pending_tweet', '', $id, $attr );
+ }
+ } else {
+ // Add shortcode arguments to provider URL.
+ add_filter( 'oembed_fetch_url', array( 'Jetpack_Tweet', 'jetpack_tweet_url_extra_args' ), 10, 3 );
+
+ /*
+ * In Jetpack, we use $wp_embed->shortcode() to return the tweet output.
+ * @see https://github.com/Automattic/jetpack/pull/11173
+ */
+ $output = $wp_embed->shortcode( $atts, $id );
+
+ // Clean up filter.
+ remove_filter( 'oembed_fetch_url', array( 'Jetpack_Tweet', 'jetpack_tweet_url_extra_args' ), 10 );
+ }
+
+ // Add Twitter widgets.js script to the footer.
+ add_action( 'wp_footer', array( 'Jetpack_Tweet', 'jetpack_tweet_shortcode_script' ) );
+
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extras', 'embeds', 'tweet' );
+
+ return $output;
+ }
+
+ /**
+ * Adds parameters to URL used to fetch the tweet.
+ *
+ * @since 4.5.0
+ *
+ * @param string $provider URL of provider that supplies the tweet we're requesting.
+ * @param string $url URL of tweet to embed.
+ * @param array $args Parameters supplied to shortcode and passed to wp_oembed_get.
+ *
+ * @return string
+ */
+ public static function jetpack_tweet_url_extra_args( $provider, $url, $args = array() ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ foreach ( self::$provider_args as $key => $value ) {
+ switch ( $key ) {
+ case 'align':
+ case 'lang':
+ case 'hide_thread':
+ case 'hide_media':
+ $provider = add_query_arg( $key, $value, $provider );
+ break;
+ }
+ }
+
+ // Disable script since we're enqueing it in our own way in the footer.
+ $provider = add_query_arg( 'omit_script', 'true', $provider );
+
+ // Twitter doesn't support maxheight so don't send it.
+ $provider = remove_query_arg( 'maxheight', $provider );
+
+ /**
+ * Filter the Twitter Partner ID.
+ *
+ * @module shortcodes
+ *
+ * @since 4.6.0
+ *
+ * @param string $partner_id Twitter partner ID.
+ */
+ $partner = apply_filters( 'jetpack_twitter_partner_id', 'jetpack' );
+
+ // Add Twitter partner ID to track embeds from Jetpack.
+ if ( ! empty( $partner ) ) {
+ $provider = add_query_arg( 'partner', $partner, $provider );
+ }
+
+ return $provider;
+ }
+
+ /**
+ * Enqueue front end assets.
+ *
+ * @since 4.5.0
+ */
+ public static function jetpack_tweet_shortcode_script() {
+ if ( ! wp_script_is( 'twitter-widgets', 'registered' ) ) {
+ wp_register_script( 'twitter-widgets', 'https://platform.twitter.com/widgets.js', array(), JETPACK__VERSION, true );
+ wp_print_scripts( 'twitter-widgets' );
+ }
+ }
+
+} // class end
diff --git a/plugins/jetpack/modules/shortcodes/twitchtv.php b/plugins/jetpack/modules/shortcodes/twitchtv.php
new file mode 100644
index 00000000..a5439d05
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/twitchtv.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * twitch.tv shortcode
+ * [twitchtv url='http://www.twitch.tv/paperbat' height='378' width='620' autoplay='false']
+ * [twitchtv url='http://www.twitch.tv/paperbat/b/323486192' height='378' width='620' autoplay='false']
+ **/
+
+/**
+ * (Live URL) http://www.twitch.tv/paperbat
+ *
+ * <iframe src="https://player.twitch.tv/?autoplay=false&#038;muted=false&#038;channel=paperbat" width="620" height="378" frameborder="0" scrolling="no" allowfullscreen></iframe>
+ *
+ * (Archive URL) http://www.twitch.tv/paperbat/v/323486192
+ *
+ * <iframe src="https://player.twitch.tv/?autoplay=false&#038;muted=false&#038;video=v323486192" width="620" height="378" frameborder="0" scrolling="no" allowfullscreen></iframe>
+ *
+ * @param $atts array User supplied shortcode arguments.
+ *
+ * @return string HTML output of the shortcode.
+ */
+function wpcom_twitchtv_shortcode( $atts ) {
+ $attr = shortcode_atts(
+ array(
+ 'height' => 378,
+ 'width' => 620,
+ 'url' => '',
+ 'autoplay' => 'false',
+ 'muted' => 'false',
+ 'time' => null,
+ ),
+ $atts
+ );
+
+ if ( empty( $attr['url'] ) ) {
+ return '<!-- Invalid twitchtv URL -->';
+ }
+
+ preg_match( '|^http://www.twitch.tv/([^/?]+)(/v/(\d+))?|i', $attr['url'], $match );
+
+ $url_args = array(
+ 'autoplay' => ( false !== $attr['autoplay'] && 'false' !== $attr['autoplay'] ) ? 'true' : 'false',
+ 'muted' => ( false !== $attr['muted'] && 'false' !== $attr['muted'] ) ? 'true' : 'false',
+ 'time' => $attr['time'],
+ );
+
+ $width = intval( $attr['width'] );
+ $height = intval( $attr['height'] );
+
+ $user_id = $match[1];
+ $video_id = 0;
+ if ( ! empty( $match[3] ) ) {
+ $video_id = (int) $match[3];
+ }
+
+ do_action( 'jetpack_bump_stats_extras', 'twitchtv', 'shortcode' );
+
+ if ( $video_id > 0 ) {
+ $url_args['video'] = 'v' . $video_id;
+ } else {
+ $url_args['channel'] = $user_id;
+ }
+
+ $url = add_query_arg( $url_args, 'https://player.twitch.tv/' );
+
+ return sprintf(
+ '<iframe src="%s" width="%d" height="%d" frameborder="0" scrolling="no" allowfullscreen></iframe>',
+ esc_url( $url ),
+ esc_attr( $width ),
+ esc_attr( $height )
+ );
+}
+
+add_shortcode( 'twitch', 'wpcom_twitchtv_shortcode' );
+add_shortcode( 'twitchtv', 'wpcom_twitchtv_shortcode' );
diff --git a/plugins/jetpack/modules/shortcodes/twitter-timeline.php b/plugins/jetpack/modules/shortcodes/twitter-timeline.php
new file mode 100644
index 00000000..38558b49
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/twitter-timeline.php
@@ -0,0 +1,56 @@
+<?php
+add_shortcode( 'twitter-timeline', 'twitter_timeline_shortcode' );
+
+function twitter_timeline_shortcode( $atts ) {
+ $default_atts = array(
+ 'username' => '',
+ 'id' => '',
+ 'width' => '450',
+ 'height' => '282',
+ );
+
+ $atts = shortcode_atts( $default_atts, $atts, 'twitter-timeline' );
+
+ $atts['username'] = preg_replace( '/[^A-Za-z0-9_]+/', '', $atts['username'] );
+
+ if ( empty( $atts['username'] ) && ! is_numeric( $atts['id'] ) ) {
+ return '<!-- ' . __( 'Must specify Twitter Timeline id or username.', 'jetpack' ) . ' -->';
+ }
+
+ $output = '<a class="twitter-timeline"';
+
+ /** This filter is documented in modules/shortcodes/tweet.php */
+ $partner = apply_filters( 'jetpack_twitter_partner_id', 'jetpack' );
+ if ( ! empty( $partner ) ) {
+ $output .= ' data-partner="' . esc_attr( $partner ) . '"';
+ }
+ if ( is_numeric( $atts['width'] ) ) {
+ $output .= ' data-width="' . esc_attr( $atts['width'] ) . '"';
+ }
+ if ( is_numeric( $atts['height'] ) ) {
+ $output .= ' data-height="' . esc_attr( $atts['height'] ) . '"';
+ }
+ if ( is_numeric( $atts['id'] ) ) {
+ $output .= ' data-widget-id="' . esc_attr( $atts['id'] ) . '"';
+ }
+ if ( ! empty( $atts['username'] ) ) {
+ $output .= ' href="' . esc_url( 'https://twitter.com/' . $atts['username'] ) . '"';
+ }
+
+ $output .= '>';
+
+ $output .= sprintf( __( 'Tweets by @%s', 'jetpack' ), $atts['username'] );
+
+ $output .= '</a>';
+
+ wp_enqueue_script( 'jetpack-twitter-timeline' );
+
+ return $output;
+}
+
+function twitter_timeline_js() {
+ if ( is_customize_preview() ) {
+ wp_enqueue_script( 'jetpack-twitter-timeline' );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'twitter_timeline_js' );
diff --git a/plugins/jetpack/modules/shortcodes/unavailable.php b/plugins/jetpack/modules/shortcodes/unavailable.php
new file mode 100644
index 00000000..3c08272f
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/unavailable.php
@@ -0,0 +1,81 @@
+<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Display a message on the frontend when we retire a shortcode,
+ * explaining why the shortcode is not available anymore.
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Class Jetpack_Shortcode_Unavailable
+ */
+class Jetpack_Shortcode_Unavailable {
+ /**
+ * Set up the actions and filters for the class to listen to.
+ *
+ * @param array $shortcodes An associative array of keys being the shortcodes that are unavailable, and a string explaining why.
+ */
+ public function __construct( $shortcodes ) {
+ $this->shortcodes = $shortcodes;
+
+ add_action( 'template_redirect', array( $this, 'add_shortcodes' ) );
+ }
+
+ /**
+ * For all of our defined unavailable shortcodes, if something else hasn't
+ * already claimed them, add a handler to nullify their output.
+ */
+ public function add_shortcodes() {
+ foreach ( array_keys( $this->shortcodes ) as $shortcode ) {
+ if ( ! shortcode_exists( $shortcode ) ) {
+ add_shortcode( $shortcode, array( $this, 'stub_shortcode' ) );
+ }
+ }
+ }
+
+ /**
+ * Nullify the output of unavailable shortcodes. Includes a filter to make
+ * it easier to notify admins that a shortcode that they used is unavailable.
+ *
+ * @param array $atts Shortcode attributes.
+ * @param string $content Post content.
+ * @param string $shortcode Shortcode name.
+ *
+ * @return mixed|void
+ */
+ public function stub_shortcode( $atts, $content = '', $shortcode = '' ) {
+ $str = '';
+ if ( current_user_can( 'edit_posts' ) && ! empty( $this->shortcodes[ $shortcode ] ) ) {
+ $str = sprintf( '<div><strong>%s</strong></div>', $this->shortcodes[ $shortcode ] );
+ }
+ /**
+ * Filter the front-end output of unavailable shortcodes.
+ *
+ * @module shortcodes
+ *
+ * @since 4.5.0
+ *
+ * @param string $str The html displayed in lieu of the shortcode.
+ * @param array $atts The attributes (numeric or named) passed to the shortcode.
+ * @param string $content The content (if any) between the opening and closing tags.
+ * @param string $shortcode The shortcode tag used to invoke this.
+ */
+ return apply_filters( 'jetpack_stub_shortcode', $str, $atts, $content, $shortcode );
+ }
+}
+
+/**
+ * Init class.
+ */
+function jetpack_init_shortcode_unavailable() {
+ new Jetpack_Shortcode_Unavailable(
+ array(
+ 'digg' => __( 'The Digg API was shut down in 2014.', 'jetpack' ),
+ 'blip.tv' => __( 'The Blip.tv service has been shut down since August 20th, 2015.', 'jetpack' ),
+ 'googlevideo' => __( 'The Google Video embed service is not available anymore, it has been replaced by YouTube.', 'jetpack' ),
+ 'jetpack-email-subscribe' => __( 'The Email Subscribe shortcode is now available as a block in the Block editor.', 'jetpack' ),
+ 'lytro' => __( 'Lytro has been shut down since March 2019.', 'jetpack' ),
+ )
+ );
+}
+add_action( 'init', 'jetpack_init_shortcode_unavailable' );
diff --git a/plugins/jetpack/modules/shortcodes/untappd-menu.php b/plugins/jetpack/modules/shortcodes/untappd-menu.php
new file mode 100644
index 00000000..f8f62f0b
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/untappd-menu.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Untappd Shortcodes
+ *
+ * @author kraftbj
+ *
+ * [untappd-menu location="123" theme="123"]
+ * @since 4.1.0
+ * @param location int Location ID for the Untappd venue. Required.
+ * @param theme int Theme ID for the Untappd menu. Required.
+ */
+
+class Jetpack_Untappd {
+
+ function __construct() {
+ add_action( 'init', array( $this, 'action_init' ) );
+ }
+
+ function action_init() {
+ add_shortcode( 'untappd-menu', array( $this, 'menu_shortcode' ) );
+ }
+
+ /**
+ * [untappd-menu] shortcode.
+ */
+ static function menu_shortcode( $atts, $content = '' ) {
+ // Let's bail if we don't have location or theme.
+ if ( ! isset( $atts['location'] ) || ! isset( $atts['theme'] ) ) {
+ if ( current_user_can( 'edit_posts' ) ) {
+ return __( 'No location or theme ID provided in the untappd-menu shortcode.', 'jetpack' );
+ }
+ return;
+ }
+
+ // Let's apply some defaults.
+ $atts = shortcode_atts(
+ array(
+ 'location' => '',
+ 'theme' => '',
+ ),
+ $atts,
+ 'untappd-menu'
+ );
+
+ // We're going to clean the user input.
+ $atts = array_map( 'absint', $atts );
+
+ if ( $atts['location'] < 1 || $atts['theme'] < 1 ) {
+ return;
+ }
+
+ static $untappd_menu = 1;
+
+ $html = '<div id="menu-container-untappd-' . $untappd_menu . '" class="untappd-menu"></div>';
+ $html .= '<script type="text/javascript">' . PHP_EOL;
+ $html .= '!function(e,n){var t=document.createElement("script"),a=document.getElementsByTagName("script")[0];' . PHP_EOL;
+ $html .= 't.async=1,a.parentNode.insertBefore(t,a),t.onload=t.onreadystatechange=function(e,a){' . PHP_EOL;
+ $html .= '(a||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t=void 0,a||n&&n())},' . PHP_EOL;
+ $html .= 't.src=e}("https://embed-menu-preloader.untappdapi.com/embed-menu-preloader.min.js",function(){' . PHP_EOL;
+ $html .= 'PreloadEmbedMenu( "menu-container-untappd-' . $untappd_menu . '",' . $atts['location'] . ',' . $atts['theme'] . ' )});' . PHP_EOL;
+ $html .= '</script>';
+
+ $untappd_menu++;
+
+ return $html;
+ }
+}
+
+new Jetpack_Untappd();
diff --git a/plugins/jetpack/modules/shortcodes/upcoming-events.php b/plugins/jetpack/modules/shortcodes/upcoming-events.php
new file mode 100644
index 00000000..02f6dda1
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/upcoming-events.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * Most of the heavy lifting done in iCalendarReader class
+ */
+class Upcoming_Events_Shortcode {
+
+ public static function init() {
+ add_shortcode( 'upcomingevents', array( __CLASS__, 'shortcode' ) );
+ }
+
+ public static function shortcode( $atts = array() ) {
+ jetpack_require_lib( 'icalendar-reader' );
+ $atts = shortcode_atts(
+ array(
+ 'url' => '',
+ 'number' => 0,
+ ),
+ $atts,
+ 'upcomingevents'
+ );
+ $args = array(
+ 'context' => 'shortcode',
+ 'number' => absint( $atts['number'] ),
+ );
+ $events = icalendar_render_events( $atts['url'], $args );
+
+ if ( ! $events ) {
+ $events = sprintf( '<p>%s</p>', __( 'No upcoming events', 'jetpack' ) );
+ }
+
+ return $events;
+ }
+}
+
+add_action( 'plugins_loaded', array( 'Upcoming_Events_Shortcode', 'init' ), 101 );
diff --git a/plugins/jetpack/modules/shortcodes/ustream.php b/plugins/jetpack/modules/shortcodes/ustream.php
new file mode 100644
index 00000000..0ab664c1
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/ustream.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * Ustream.tv shortcode
+ *
+ * Example:
+ * [ustream id=1524 live=1]
+ * [ustreamsocial id=12980237 width="500"]
+ *
+ * Embed code example, from http://www.ustream.tv/leolaporte
+ * <iframe src="http://www.ustream.tv/embed/recorded/1524?v=3&#038;wmode=direct" width="480" height="296" scrolling="no" frameborder="0" style="border: 0 none transparent;"></iframe>
+ *
+ * @package Jetpack
+ */
+
+add_shortcode( 'ustream', 'ustream_shortcode' );
+add_shortcode( 'ustreamsocial', 'ustreamsocial_shortcode' );
+
+/**
+ * Parse shortcode arguments and render output for ustream single video.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts array of user-supplied arguments.
+ *
+ * @return string HTML output.
+ */
+function ustream_shortcode( $atts ) {
+ if ( isset( $atts[0] ) ) {
+ return '<!-- ustream error: bad parameters -->';
+ }
+
+ $defaults = array(
+ 'width' => 480,
+ 'height' => 296,
+ 'id' => 0,
+ 'live' => 0,
+ 'highlight' => 0,
+ 'version' => 3,
+ 'hwaccel' => 1,
+ );
+ $atts = array_map( 'intval', shortcode_atts( $defaults, $atts ) );
+
+ if ( 0 >= $atts['id'] ) {
+ return '<!-- ustream error: bad video ID -->';
+ }
+
+ if ( 0 >= $atts['height'] ) {
+ return '<!-- ustream error: height invalid -->';
+ }
+
+ if ( 0 >= $atts['width'] ) {
+ return '<!-- ustream error: width invalid -->';
+ }
+
+ if ( $atts['live'] ) {
+ $recorded = '';
+ } else {
+ $recorded = 'recorded/';
+ }
+
+ if ( ! $atts['live'] && ( 0 < $atts['highlight'] ) ) {
+ $highlight = sprintf( '/highlight/%d', esc_attr( $atts['highlight'] ) );
+ } else {
+ $highlight = '';
+ }
+
+ $url_base = sprintf(
+ 'https://www.ustream.tv/embed/%s%d%s',
+ $recorded,
+ esc_attr( $atts['id'] ),
+ $highlight
+ );
+
+ $video_options = array(
+ 'html5ui' => 1,
+ 'v' => absint( $atts['version'] ),
+ );
+
+ if ( 0 < $atts['hwaccel'] ) {
+ $video_options['wmode'] = 'direct';
+ }
+
+ $url = add_query_arg(
+ $video_options,
+ $url_base
+ );
+
+ $output = sprintf(
+ '<iframe src="%1$s" width="%2$d" height="%3$d" scrolling="no" style="border: 0 none transparent;"></iframe>',
+ esc_url( $url ),
+ absint( $atts['width'] ),
+ absint( $atts['height'] )
+ );
+
+ return $output;
+}
+
+/**
+ * Parse shortcode arguments and render output for ustream's Social Stream.
+ *
+ * @since 4.5.0
+ *
+ * @param array $atts array of user-supplied arguments.
+ *
+ * @return string HTML output.
+ */
+function ustreamsocial_shortcode( $atts ) {
+ $defaults = array(
+ 'id' => 0,
+ 'height' => 420,
+ 'width' => 320,
+ );
+ $atts = array_map( 'intval', shortcode_atts( $defaults, $atts ) );
+
+ if ( 0 >= $atts['id'] ) {
+ return '<!-- ustreamsocial error: bad social stream ID -->';
+ }
+
+ if ( 0 >= $atts['height'] ) {
+ return '<!-- ustreamsocial error: height invalid -->';
+ }
+
+ if ( 0 >= $atts['width'] ) {
+ return '<!-- ustreamsocial error: width invalid -->';
+ }
+
+ $url = 'https://www.ustream.tv/socialstream/' . esc_attr( $atts['id'] );
+
+ return sprintf(
+ '<iframe id="SocialStream" src="%1$s" class="" name="SocialStream" width="%2$d" height="%3$d" scrolling="no" allowtransparency="true" style="visibility: visible; margin-top: 0; margin-bottom: 0; border: 0;"></iframe>',
+ esc_url( $url ),
+ absint( $atts['width'] ),
+ absint( $atts['height'] )
+ );
+}
diff --git a/plugins/jetpack/modules/shortcodes/videopress.php b/plugins/jetpack/modules/shortcodes/videopress.php
new file mode 100644
index 00000000..a4bfd167
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/videopress.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Provides VideoPress videos support when module is disabled.
+ *
+ * @since 2.4
+ * @since 3.9.5 Added compatibility with refactored VideoPress module.
+ */
+
+if ( ! Jetpack::is_module_active( 'videopress' ) ) {
+
+ Jetpack::dns_prefetch(
+ array(
+ '//v0.wordpress.com',
+ )
+ );
+
+ include_once JETPACK__PLUGIN_DIR . 'modules/videopress/utility-functions.php';
+ include_once JETPACK__PLUGIN_DIR . 'modules/videopress/shortcode.php';
+
+}
diff --git a/plugins/jetpack/modules/shortcodes/vimeo.php b/plugins/jetpack/modules/shortcodes/vimeo.php
new file mode 100644
index 00000000..91492d9c
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/vimeo.php
@@ -0,0 +1,301 @@
+<?php
+
+/*
+[vimeo 141358]
+[vimeo http://vimeo.com/141358]
+[vimeo 141358 h=500&w=350]
+[vimeo id=141358 width=350 height=500]
+
+<iframe src="http://player.vimeo.com/video/18427511" width="400" height="225" frameborder="0"></iframe><p><a href="http://vimeo.com/18427511">Eskmo 'We Got More' (Official Video)</a> from <a href="http://vimeo.com/ninjatune">Ninja Tune</a> on <a href="http://vimeo.com">Vimeo</a>.</p>
+*/
+
+function jetpack_shortcode_get_vimeo_id( $atts ) {
+ if ( isset( $atts[0] ) ) {
+ $atts[0] = trim( $atts[0], '=' );
+ $id = false;
+ if ( is_numeric( $atts[0] ) ) {
+ $id = (int) $atts[0];
+ } elseif ( preg_match( '|vimeo\.com/(\d+)/?$|i', $atts[0], $match ) ) {
+ $id = (int) $match[1];
+ } elseif ( preg_match( '|player\.vimeo\.com/video/(\d+)/?$|i', $atts[0], $match ) ) {
+ $id = (int) $match[1];
+ }
+
+ return $id;
+ }
+
+ return 0;
+}
+
+/**
+ * Convert a Vimeo shortcode into an embed code.
+ *
+ * @param array $atts An array of shortcode attributes.
+ *
+ * @return string The embed code for the Vimeo video.
+ */
+function vimeo_shortcode( $atts ) {
+ global $content_width;
+
+ $attr = array_map(
+ 'intval',
+ shortcode_atts(
+ array(
+ 'id' => 0,
+ 'width' => 0,
+ 'height' => 0,
+ 'autoplay' => 0,
+ 'loop' => 0,
+ ),
+ $atts
+ )
+ );
+
+ if ( isset( $atts[0] ) ) {
+ $attr['id'] = jetpack_shortcode_get_vimeo_id( $atts );
+ }
+
+ if ( ! $attr['id'] ) {
+ return '<!-- vimeo error: not a vimeo video -->';
+ }
+
+ // [vimeo 141358 h=500&w=350]
+ $params = shortcode_new_to_old_params( $atts ); // h=500&w=350
+ $params = str_replace( array( '&amp;', '&#038;' ), '&', $params );
+ parse_str( $params, $args );
+
+ $width = intval( $attr['width'] );
+ $height = intval( $attr['height'] );
+
+ // Support w and h argument as fallback.
+ if ( empty( $width ) && isset( $args['w'] ) ) {
+ $width = intval( $args['w'] );
+
+ if ( empty( $height ) && ! isset( $args['h'] ) ) {
+ // The case where w=300 is specified without h=200, otherwise $height
+ // will always equal the default of 300, no matter what w was set to.
+ $height = round( ( $width / 640 ) * 360 );
+ }
+ }
+
+ if ( empty( $height ) && isset( $args['h'] ) ) {
+ $height = (int) $args['h'];
+
+ if ( ! isset( $args['w'] ) ) {
+ $width = round( ( $height / 360 ) * 640 );
+ }
+ }
+
+ if ( ! $width && ! empty( $content_width ) ) {
+ $width = absint( $content_width );
+ }
+
+ // If setting the width with content_width has failed, defaulting
+ if ( ! $width ) {
+ $width = 640;
+ }
+
+ if ( ! $height ) {
+ $height = round( ( $width / 640 ) * 360 );
+ }
+
+ /**
+ * Filter the Vimeo player width.
+ *
+ * @module shortcodes
+ *
+ * @since 3.4.0
+ *
+ * @param int $width Width of the Vimeo player in pixels.
+ */
+ $width = (int) apply_filters( 'vimeo_width', $width );
+
+ /**
+ * Filter the Vimeo player height.
+ *
+ * @module shortcodes
+ *
+ * @since 3.4.0
+ *
+ * @param int $height Height of the Vimeo player in pixels.
+ */
+ $height = (int) apply_filters( 'vimeo_height', $height );
+
+ $url = esc_url( 'https://player.vimeo.com/video/' . $attr['id'] );
+
+ // Handle autoplay and loop arguments.
+ if (
+ isset( $args['autoplay'] ) && '1' === $args['autoplay'] // Parsed from the embedded URL.
+ || $attr['autoplay'] // Parsed from shortcode arguments.
+ || in_array( 'autoplay', $atts ) // Catch the argument passed without a value.
+ ) {
+ $url = add_query_arg( 'autoplay', 1, $url );
+ }
+
+ if (
+ isset( $args['loop'] ) && '1' === $args['loop'] // Parsed from the embedded URL.
+ || $attr['loop'] // Parsed from shortcode arguments.
+ || in_array( 'loop', $atts ) // Catch the argument passed without a value.
+ ) {
+ $url = add_query_arg( 'loop', 1, $url );
+ }
+
+ $html = sprintf(
+ '<div class="embed-vimeo" style="text-align: center;"><iframe src="%1$s" width="%2$u" height="%3$u" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div>',
+ esc_url( $url ),
+ esc_attr( $width ),
+ esc_attr( $height )
+ );
+
+ /**
+ * Filter the Vimeo player HTML.
+ *
+ * @module shortcodes
+ *
+ * @since 1.2.3
+ *
+ * @param string $html Embedded Vimeo player HTML.
+ */
+ $html = apply_filters( 'video_embed_html', $html );
+
+ return $html;
+}
+
+add_shortcode( 'vimeo', 'vimeo_shortcode' );
+
+/**
+ * Callback to modify output of embedded Vimeo video using Jetpack's shortcode.
+ *
+ * @since 3.9
+ *
+ * @param array $matches Regex partial matches against the URL passed.
+ * @param array $attr Attributes received in embed response
+ * @param array $url Requested URL to be embedded
+ *
+ * @return string Return output of Vimeo shortcode with the proper markup.
+ */
+function wpcom_vimeo_embed_url( $matches, $attr, $url ) {
+ return vimeo_shortcode( array( $url ) );
+}
+
+/**
+ * For bare URLs on their own line of the form
+ * http://vimeo.com/12345
+ *
+ * @since 3.9
+ *
+ * @uses wpcom_vimeo_embed_url
+ */
+function wpcom_vimeo_embed_url_init() {
+ wp_embed_register_handler( 'wpcom_vimeo_embed_url', '#https?://(.+\.)?vimeo\.com/#i', 'wpcom_vimeo_embed_url' );
+}
+
+// Register handler to modify Vimeo embeds using Jetpack's shortcode output.
+add_action( 'init', 'wpcom_vimeo_embed_url_init' );
+
+function vimeo_embed_to_shortcode( $content ) {
+ if ( ! is_string( $content ) || false === stripos( $content, 'player.vimeo.com/video/' ) ) {
+ return $content;
+ }
+
+ $regexp = '!<iframe\s+src=[\'"](https?:)?//player\.vimeo\.com/video/(\d+)[\w=&;?]*[\'"]((?:\s+\w+=[\'"][^\'"]*[\'"])*)((?:[\s\w]*))></iframe>!i';
+ $regexp_ent = str_replace( '&amp;#0*58;', '&amp;#0*58;|&#0*58;', htmlspecialchars( $regexp, ENT_NOQUOTES ) );
+
+ foreach ( array( 'regexp', 'regexp_ent' ) as $reg ) {
+ if ( ! preg_match_all( $$reg, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ $id = (int) $match[2];
+
+ $params = $match[3];
+
+ if ( 'regexp_ent' == $reg ) {
+ $params = html_entity_decode( $params );
+ }
+
+ $params = wp_kses_hair( $params, array( 'http' ) );
+
+ $width = isset( $params['width'] ) ? (int) $params['width']['value'] : 0;
+ $height = isset( $params['height'] ) ? (int) $params['height']['value'] : 0;
+
+ $wh = '';
+ if ( $width && $height ) {
+ $wh = ' w=' . $width . ' h=' . $height;
+ }
+
+ $shortcode = '[vimeo ' . $id . $wh . ']';
+ $content = str_replace( $match[0], $shortcode, $content );
+ }
+ }
+
+ return $content;
+}
+
+add_filter( 'pre_kses', 'vimeo_embed_to_shortcode' );
+
+/**
+ * Replaces shortcodes and plain-text URLs to Vimeo videos with Vimeo embeds.
+ * Covers shortcode usage [vimeo 1234] | [vimeo https://vimeo.com/1234] | [vimeo http://vimeo.com/1234]
+ * Or plain text URLs https://vimeo.com/1234 | vimeo.com/1234 | //vimeo.com/1234
+ * Links are left intact.
+ *
+ * @since 3.7.0
+ * @since 3.9.5 One regular expression matches shortcodes and plain URLs.
+ *
+ * @param string $content HTML content
+ * @return string The content with embeds instead of URLs
+ */
+function vimeo_link( $content ) {
+ /**
+ * [vimeo 12345]
+ * [vimeo http://vimeo.com/12345]
+ */
+ $shortcode = '(?:\[vimeo\s+[^0-9]*)([0-9]+)(?:\])';
+
+ /**
+ * http://vimeo.com/12345
+ * https://vimeo.com/12345
+ * //vimeo.com/12345
+ * vimeo.com/some/descender/12345
+ *
+ * Should not capture inside HTML attributes
+ * [Not] <a href="vimeo.com/12345">Cool Video</a>
+ * [Not] <a href="https://vimeo.com/12345">vimeo.com/12345</a>
+ *
+ * Could erroneously capture:
+ * <a href="some.link/maybe/even/vimeo">This video (vimeo.com/12345) is teh cat's meow!</a>
+ */
+ $plain_url = "(?:[^'\">]?\/?(?:https?:\/\/)?vimeo\.com[^0-9]+)([0-9]+)(?:[^'\"0-9<]|$)";
+
+ return jetpack_preg_replace_callback_outside_tags(
+ sprintf( '#%s|%s#i', $shortcode, $plain_url ),
+ 'vimeo_link_callback',
+ $content,
+ 'vimeo'
+ );
+}
+
+/**
+ * Callback function for the regex that replaces Vimeo URLs with Vimeo embeds.
+ *
+ * @since 3.7.0
+ *
+ * @param array $matches An array containing a Vimeo URL.
+ * @return string The Vimeo HTML embed code.
+ */
+function vimeo_link_callback( $matches ) {
+ $id = isset( $matches[2] ) ? $matches[2] : $matches[1];
+ if ( isset( $id ) && ctype_digit( $id ) ) {
+ return "\n" . vimeo_shortcode( array( 'id' => $id ) ) . "\n";
+ }
+ return $matches[0];
+}
+
+/** This filter is documented in modules/shortcodes/youtube.php */
+if ( ! is_admin() && apply_filters( 'jetpack_comments_allow_oembed', true ) ) {
+ // We attach wp_kses_post to comment_text in default-filters.php with priority of 10 anyway, so the iframe gets filtered out.
+ // Higher priority because we need it before auto-link and autop get to it
+ add_filter( 'comment_text', 'vimeo_link', 1 );
+}
diff --git a/plugins/jetpack/modules/shortcodes/vine.php b/plugins/jetpack/modules/shortcodes/vine.php
new file mode 100644
index 00000000..444b9999
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/vine.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Vine shortcode
+ */
+
+/**
+ * Vine embed code:
+ * <iframe class="vine-embed" src="https://vine.co/v/bjHh0zHdgZT" width="600" height="600" frameborder="0"></iframe>
+ * <script async src="//platform.vine.co/static/scripts/embed.js" charset="utf-8"></script>
+ *
+ * URL example:
+ * https://vine.co/v/bjHh0zHdgZT/
+ *
+ * Embed shortcode examples:
+ * [embed]https://vine.co/v/bjHh0zHdgZT[/embed]
+ * [embed width="300"]https://vine.co/v/bjHh0zHdgZT[/embed]
+ * [embed type="postcard" width="300"]https://vine.co/v/bjHh0zHdgZT[/embed]
+ **/
+
+function vine_embed_video( $matches, $attr, $url, $rawattr ) {
+ static $vine_flag_embedded_script;
+
+ $max_height = 300;
+ $type = 'simple';
+
+ // Only allow 'postcard' or 'simple' types
+ if ( isset( $rawattr['type'] ) && $rawattr['type'] === 'postcard' ) {
+ $type = 'postcard';
+ }
+
+ $vine_size = Jetpack::get_content_width();
+
+ // If the user enters a value for width or height, we ignore the Jetpack::get_content_width()
+ if ( isset( $rawattr['width'] ) || isset( $rawattr['height'] ) ) {
+ // 300 is the minimum size that Vine provides for embeds. Lower than that, the postcard embeds looks weird.
+ $vine_size = max( $max_height, min( $attr['width'], $attr['height'] ) );
+ }
+
+ if ( empty( $vine_size ) ) {
+ $vine_size = $max_height;
+ }
+
+ $url = 'https://vine.co/v/' . $matches[1] . '/embed/' . $type;
+ $vine_html = sprintf( '<span class="embed-vine" style="display: block;"><iframe class="vine-embed" src="%s" width="%s" height="%s" frameborder="0"></iframe></span>', esc_url( $url ), (int) $vine_size, (int) $vine_size );
+
+ if ( $vine_flag_embedded_script !== true ) {
+ $vine_html .= '<script async src="//platform.vine.co/static/scripts/embed.js" charset="utf-8"></script>';
+ $vine_flag_embedded_script = true;
+ }
+
+ return $vine_html;
+}
+wp_embed_register_handler( 'jetpack_vine', '#https?://vine.co/v/([a-z0-9]+).*#i', 'vine_embed_video' );
+
+function vine_shortcode( $atts ) {
+ global $wp_embed;
+
+ if ( empty( $atts['url'] ) ) {
+ return '';
+ }
+
+ if ( ! preg_match( '#https?://vine.co/v/([a-z0-9]+).*#i', $atts['url'] ) ) {
+ return '';
+ }
+
+ return $wp_embed->shortcode( $atts, $atts['url'] );
+}
+add_shortcode( 'vine', 'vine_shortcode' );
diff --git a/plugins/jetpack/modules/shortcodes/vr.php b/plugins/jetpack/modules/shortcodes/vr.php
new file mode 100644
index 00000000..419af37b
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/vr.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * VR Viewer Shortcode
+ * converts [vr] shortcode to an iframe viewer hosted on vr.me.sh
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Scrub URL paramaters for VR viewer
+ *
+ * @param array $params {
+ * parameter array which is passed to the jetpack_vr_viewer.
+ *
+ * @type string $url url of 360 media
+ * @type string $guid guid for videopress
+ * @type string $view cinema, 360 - controls if panaroma view, or 360
+ * @type string $rotation number for rotating media
+ * @type string $preview show preview image or not
+ * }
+ *
+ * @return array|false $url_params Array of URL parameters.
+ */
+function jetpack_vr_viewer_get_viewer_url_params( $params ) {
+ $url_params = array();
+
+ if ( isset( $params['rotation'] ) ) {
+ $url_params['rotation'] = intval( $params['rotation'], 10 );
+ }
+
+ if ( isset( $params['view'] ) && in_array( $params['view'], array( 'cinema', '360' ), true ) ) {
+ $url_params['view'] = $params['view'];
+ }
+
+ if ( isset( $params['preview'] ) && $params['preview'] ) {
+ $url_params['preview'] = 1;
+ }
+
+ if ( isset( $params['url'] ) ) {
+ return array_merge( $url_params, array( 'url' => $params['url'] ) );
+ } elseif ( isset( $params['guid'] ) ) {
+ return array_merge( $url_params, array( 'guid' => $params['guid'] ) );
+ }
+
+ return false;
+}
+
+/**
+ * Get padding for IFRAME depending on view type
+ *
+ * @param string $view string cinema, 360 - default cinema.
+ *
+ * @return string $css padding
+ */
+function jetpack_vr_viewer_iframe_padding( $view ) {
+ if ( '360' === $view ) {
+ return '100%'; // 1:1 square aspect for 360
+ }
+
+ return '50%'; // 2:1 panorama aspect
+}
+
+/**
+ * Create HTML for VR Viewer IFRAME and wrapper
+ * The viewer code is hosted on vr.me.sh site which is then displayed
+ * within posts via an IFRAME. This function returns the IFRAME html.
+ *
+ * @param array $url_params {
+ * parameter array which is passed to the jetpack_vr_viewer.
+ *
+ * @type string $url url of 360 media
+ * @type string $guid guid for videopress
+ * @type string $view cinema, 360 - controls if panaroma view, or 360
+ * @type string $rotation number for rotating media
+ * @type string $preview show preview image or not
+ * }
+ *
+ * @return string $rtn an iframe for viewer.
+ */
+function jetpack_vr_viewer_get_html( $url_params ) {
+ global $content_width;
+
+ $iframe = add_query_arg( $url_params, 'https://vr.me.sh/view/' );
+
+ // set some defaults.
+ $maxwidth = ( isset( $content_width ) ) ? $content_width : 720;
+ $view = ( isset( $url_params['view'] ) ) ? $url_params['view'] : 'cinema';
+
+ $rtn = '<div style="position: relative; max-width: ' . $maxwidth . 'px; margin-left: auto; margin-right: auto; overflow: hidden; margin-bottom: 1em;">';
+ $rtn .= '<div style="padding-top: ' . jetpack_vr_viewer_iframe_padding( $view ) . ';"></div>';
+ $rtn .= '<iframe style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; height: 100%" allowfullscreen="true" frameborder="0" width="100%" height="300" src="' . esc_url( $iframe ) . '">';
+ $rtn .= '</iframe>';
+ $rtn .= '</div>';
+
+ return $rtn;
+}
+
+/**
+ * Convert [vr] shortcode to viewer
+ *
+ * Shortcode example:
+ * [vr url="https://en-blog.files.wordpress.com/2016/12/regents_park.jpg" view="360"]
+ *
+ * VR Viewer embed code:
+ * <div style="position: relative; max-width: 720px; margin-left: auto; margin-right: auto; overflow: hidden;">
+ * <div style="padding-top: 100%;"></div>
+ * <iframe style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; height: 100%" allowfullscreen="true" frameborder="0" width="100%" height="400" src="https://vr.me.sh/view/?view=360&amp;url=https://en-blog.files.wordpress.com/2016/12/regents_park.jpg">
+ * </iframe>
+ * </div>
+ *
+ * @param array $atts Shortcode attributes.
+ *
+ * @return html - complete vr viewer html
+ */
+function jetpack_vr_viewer_shortcode( $atts ) {
+ $params = shortcode_atts(
+ array(
+ 0 => null,
+ 'url' => null,
+ 'src' => null,
+ 'guid' => null,
+ 'rotation' => null,
+ 'view' => null,
+ 'preview' => false,
+ ),
+ $atts
+ );
+
+ // We offer a few ways to specify the URL.
+ if ( $params[0] ) {
+ $params['url'] = $params[0];
+ } elseif ( $params['src'] ) {
+ $params['url'] = $params['src'];
+ }
+
+ $url_params = jetpack_vr_viewer_get_viewer_url_params( $params );
+ if ( $url_params ) {
+ return jetpack_vr_viewer_get_html( $url_params );
+ }
+
+ // add check for user.
+ if ( current_user_can( 'edit_posts' ) ) {
+ return '[vr] shortcode requires a data source to be given';
+ } else {
+ return '';
+ }
+}
+add_shortcode( 'vr', 'jetpack_vr_viewer_shortcode' );
diff --git a/plugins/jetpack/modules/shortcodes/wordads.php b/plugins/jetpack/modules/shortcodes/wordads.php
new file mode 100644
index 00000000..bca6c48a
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/wordads.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * Embed WordAds 'ad' in post
+ */
+class Jetpack_WordAds_Shortcode {
+
+ private $scripts_and_style_included = false;
+
+ function __construct() {
+ add_action( 'init', array( $this, 'action_init' ) );
+ }
+
+ /**
+ * Register our shortcode and enqueue necessary files.
+ */
+ function action_init() {
+ global $wordads;
+
+ if ( empty( $wordads ) ) {
+ return null;
+ }
+
+ add_shortcode( 'wordads', array( $this, 'wordads_shortcode' ) );
+ }
+
+ /**
+ * Our [wordads] shortcode.
+ * Prints a WordAds Ad.
+ *
+ * @param array $atts Array of shortcode attributes.
+ * @param string $content Post content.
+ *
+ * @return string HTML for WordAds shortcode.
+ */
+ static function wordads_shortcode( $atts, $content = '' ) {
+ $atts = shortcode_atts( array(), $atts, 'wordads' );
+
+ return self::wordads_shortcode_html( $atts, $content );
+ }
+
+ /**
+ * The shortcode output
+ *
+ * @param array $atts Array of shortcode attributes.
+ * @param string $content Post content.
+ *
+ * @return string HTML output
+ */
+ static function wordads_shortcode_html( $atts, $content = '' ) {
+ global $wordads;
+
+ if ( empty( $wordads ) ) {
+ return '<div>' . __( 'The WordAds module is not active', 'jetpack' ) . '</div>';
+ }
+
+ $html = '<div class="jetpack-wordad" itemscope itemtype="https://schema.org/WPAdBlock"></div>';
+ $html = $wordads->insert_inline_ad( $html );
+
+ return $html;
+ }
+}
+
+new Jetpack_WordAds_Shortcode();
diff --git a/plugins/jetpack/modules/shortcodes/wufoo.php b/plugins/jetpack/modules/shortcodes/wufoo.php
new file mode 100644
index 00000000..fbe6fe70
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/wufoo.php
@@ -0,0 +1,89 @@
+<?php
+/*
+Plugin Name: Wufoo Shortcode Plugin
+Description: Enables shortcode to embed Wufoo forms. Usage: [wufoo username="chriscoyier" formhash="x7w3w3" autoresize="true" height="458" header="show"]
+Author: Chris Coyier / Wufoo, evansolomon
+
+Based on https://wordpress.org/extend/plugins/wufoo-shortcode/
+https://wufoo.com/docs/code-manager/wordpress-shortcode-plugin/
+*/
+
+
+function wufoo_shortcode( $atts ) {
+ $attr = shortcode_atts(
+ array(
+ 'username' => '',
+ 'formhash' => '',
+ 'autoresize' => true,
+ 'height' => '500',
+ 'header' => 'show',
+ ),
+ $atts
+ );
+
+ // Check username and formhash to ensure they only have alphanumeric characters or underscores, and aren't empty.
+ if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $attr['username'] ) || ! preg_match( '/^[a-zA-Z0-9_]+$/', $attr['formhash'] ) ) {
+
+ /**
+ * Return an error to the users with instructions if one of these params is invalid
+ * They don't have default values because they are user/form-specific
+ */
+ $return_error = sprintf( __( 'Something is wrong with your Wufoo shortcode. If you copy and paste it from the %1$sWufoo Code Manager%2$s, you should be golden.', 'jetpack' ), '<a href="https://wufoo.com/docs/code-manager/" target="_blank">', '</a>' );
+
+ return '
+ <div style="border: 20px solid red; border-radius: 40px; padding: 40px; margin: 50px 0 70px;">
+ <h3>Uh oh!</h3>
+ <p style="margin: 0;">' . $return_error . '</p>
+ </div>';
+ }
+
+ /**
+ * Placeholder which will tell Wufoo where to render the form.
+ */
+ $js_embed_placeholder = '<div id="wufoo-' . $attr['formhash'] . '"></div>';
+
+ /**
+ * Required parameters are present.
+ * An error will be returned inside the form if they are invalid.
+ */
+ $js_embed = '(function(){try{var wufoo_' . $attr['formhash'] . ' = new WufooForm();';
+ $js_embed .= 'wufoo_' . $attr['formhash'] . '.initialize({';
+ $js_embed .= "'userName':'" . $attr['username'] . "', ";
+ $js_embed .= "'formHash':'" . $attr['formhash'] . "', ";
+ $js_embed .= "'autoResize':" . (bool) ( $attr['autoresize'] ) . ',';
+ $js_embed .= "'height':'" . (int) $attr['height'] . "',";
+ $js_embed .= "'header':'" . esc_js( $attr['header'] ) . "',";
+ $js_embed .= "'ssl':true,'async':true});";
+ $js_embed .= 'wufoo_' . $attr['formhash'] . '.display();';
+ $js_embed .= '}catch(e){}})();';
+
+ /**
+ * iframe embed, loaded inside <noscript> tags.
+ */
+ $iframe_embed = '<iframe ';
+ $iframe_embed .= 'height="' . (int) $attr['height'] . '" ';
+ $iframe_embed .= 'allowTransparency="true" frameborder="0" scrolling="no" style="width:100%;border:none;"';
+ $iframe_embed .= 'src="https://' . $attr['username'] . '.wufoo.com/embed/' . $attr['formhash'] . '/">';
+ $iframe_embed .= '<a href="https://' . $attr['username'] . '.wufoo.com/forms/' . $attr['formhash'] . '/" ';
+ $iframe_embed .= 'rel="nofollow" target="_blank">' . __( 'Fill out my Wufoo form!', 'jetpack' ) . '</a></iframe>';
+
+ wp_enqueue_script(
+ 'wufoo-form',
+ 'https://www.wufoo.com/scripts/embed/form.js',
+ array(),
+ false,
+ true
+ );
+
+ wp_add_inline_script( 'wufoo-form', $js_embed );
+
+ /** This action is already documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'embeds', 'wufoo' );
+
+ /**
+ * Return embed in JS and iframe.
+ */
+ return "$js_embed_placeholder<noscript>$iframe_embed</noscript>";
+}
+
+add_shortcode( 'wufoo', 'wufoo_shortcode' );
diff --git a/plugins/jetpack/modules/shortcodes/youtube.php b/plugins/jetpack/modules/shortcodes/youtube.php
new file mode 100644
index 00000000..048eb09b
--- /dev/null
+++ b/plugins/jetpack/modules/shortcodes/youtube.php
@@ -0,0 +1,396 @@
+<?php
+
+/**
+ * youtube shortcode
+ *
+ * Contains shortcode + some improvements over the Embeds syntax @
+ * http://codex.wordpress.org/Embeds
+ *
+ * @example [youtube=http://www.youtube.com/watch?v=wq0rXGLs0YM&amp;fs=1&amp;hl=bg_BG]
+ */
+
+/**
+ * Replaces YouTube embeds with YouTube shortcodes.
+ *
+ * @param string $content HTML content.
+ * @return string The content with YouTube embeds replaced with YouTube shortcodes.
+ */
+// 2008-07-15:
+// <object width="425" height="344"><param name="movie" value="http://www.youtube.com/v/bZBHZT3a-FA&hl=en&fs=1"></param><param name="allowFullScreen" value="true"></param><embed src="http://www.youtube.com/v/bZBHZT3a-FA&hl=en&fs=1" type="application/x-shockwave-flash" allowfullscreen="true" width="425" height="344"></embed></object>
+// around 2008-06-06 youtube changed their old embed code to this:
+// <object width="425" height="344"><param name="movie" value="http://www.youtube.com/v/M1D30gS7Z8U&hl=en"></param><embed src="http://www.youtube.com/v/M1D30gS7Z8U&hl=en" type="application/x-shockwave-flash" width="425" height="344"></embed></object>
+// old style was:
+// <object width="425" height="344"><param name="movie" value="http://www.youtube.com/v/dGY28Qbj76A&rel=0"></param><param name="wmode" value="transparent"></param><embed src="http://www.youtube.com/v/dGY28Qbj76A&rel=0" type="application/x-shockwave-flash" wmode="transparent" width="425" height="344"></embed></object>
+// 12-2010:
+// <object width="640" height="385"><param name="movie" value="http://www.youtube.com/v/3H8bnKdf654?fs=1&amp;hl=en_GB"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/3H8bnKdf654?fs=1&amp;hl=en_GB" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="640" height="385"></embed></object>
+// 01-2011:
+// <iframe title="YouTube video player" class="youtube-player" type="text/html" width="640" height="390" src="http://www.youtube.com/embed/Qq9El3ki0_g" frameborder="0" allowFullScreen></iframe>
+// <iframe class="youtube-player" type="text/html" width="640" height="385" src="http://www.youtube.com/embed/VIDEO_ID" frameborder="0"></iframe>
+function youtube_embed_to_short_code( $content ) {
+ if ( ! is_string( $content ) || false === strpos( $content, 'youtube.com' ) ) {
+ return $content;
+ }
+
+ // older codes
+ $regexp = '!<object(.*?)>.*?<param\s+name=[\'"]movie[\'"]\s+value=[\'"](https?:)?//www\.youtube\.com/v/([^\'"]+)[\'"].*?>.*?</object>!i';
+ $regexp_ent = htmlspecialchars( $regexp, ENT_NOQUOTES );
+ $old_regexp = '!<embed(?:\s+\w+="[^"]*")*\s+src="https?(?:\:|&#0*58;)//www\.youtube\.com/v/([^"]+)"(?:\s+\w+="[^"]*")*\s*(?:/>|>\s*</embed>)!';
+ $old_regexp_ent = str_replace( '&amp;#0*58;', '&amp;#0*58;|&#0*58;', htmlspecialchars( $old_regexp, ENT_NOQUOTES ) );
+
+ // new code
+ $ifr_regexp = '!<iframe((?:\s+\w+="[^"]*")*?)\s+src="(https?:)?//(?:www\.)*youtube.com/embed/([^"]+)".*?</iframe>!i';
+ $ifr_regexp_ent = str_replace( '&amp;#0*58;', '&amp;#0*58;|&#0*58;', htmlspecialchars( $ifr_regexp, ENT_NOQUOTES ) );
+
+ foreach ( array( 'regexp', 'regexp_ent', 'old_regexp', 'old_regexp_ent', 'ifr_regexp', 'ifr_regexp_ent' ) as $reg ) {
+ if ( ! preg_match_all( $$reg, $content, $matches, PREG_SET_ORDER ) ) {
+ continue;
+ }
+
+ foreach ( $matches as $match ) {
+ // Hack, but '?' should only ever appear once, and
+ // it should be for the 1st field-value pair in query string,
+ // if it is present
+ // YouTube changed their embed code.
+ // Example of how it is now:
+ // <object width="640" height="385"><param name="movie" value="http://www.youtube.com/v/aP9AaD4tgBY?fs=1&amp;hl=en_US"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/aP9AaD4tgBY?fs=1&amp;hl=en_US" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="640" height="385"></embed></object>
+ // As shown at the start of function, previous YouTube didn't '?'
+ // the 1st field-value pair.
+ if ( in_array( $reg, array( 'ifr_regexp', 'ifr_regexp_ent', 'regexp', 'regexp_ent' ) ) ) {
+ $params = $match[1];
+
+ if ( in_array( $reg, array( 'ifr_regexp_ent', 'regexp_ent' ) ) ) {
+ $params = html_entity_decode( $params );
+ }
+
+ $params = wp_kses_hair( $params, array( 'http' ) );
+
+ $width = isset( $params['width'] ) ? (int) $params['width']['value'] : 0;
+ $height = isset( $params['height'] ) ? (int) $params['height']['value'] : 0;
+ $wh = '';
+
+ if ( $width && $height ) {
+ $wh = "&w=$width&h=$height";
+ }
+
+ $url = esc_url_raw( "https://www.youtube.com/watch?v={$match[3]}{$wh}" );
+ } else {
+ $match[1] = str_replace( '?', '&', $match[1] );
+
+ $url = esc_url_raw( 'https://www.youtube.com/watch?v=' . html_entity_decode( $match[1] ) );
+ }
+
+ $content = str_replace( $match[0], "[youtube $url]", $content );
+
+ /**
+ * Fires before the YouTube embed is transformed into a shortcode.
+ *
+ * @module shortcodes
+ *
+ * @since 1.2.0
+ *
+ * @param string youtube Shortcode name.
+ * @param string $url YouTube video URL.
+ */
+ do_action( 'jetpack_embed_to_shortcode', 'youtube', $url );
+ }
+ }
+
+ return $content;
+}
+
+add_filter( 'pre_kses', 'youtube_embed_to_short_code' );
+
+/**
+ * Replaces plain-text links to YouTube videos with YouTube embeds.
+ *
+ * @param string $content HTML content
+ * @return string The content with embeds instead of URLs
+ */
+function youtube_link( $content ) {
+ return jetpack_preg_replace_callback_outside_tags( '!(?:\n|\A)https?://(?:www\.)?(?:youtube.com/(?:v/|playlist|watch[/\#?])|youtu\.be/)[^\s]+?(?:\n|\Z)!i', 'youtube_link_callback', $content, 'youtube.com/' );
+}
+
+/**
+ * Callback function for the regex that replaces YouTube URLs with
+ * YouTube embeds.
+ */
+function youtube_link_callback( $matches ) {
+ return "\n" . youtube_id( $matches[0] ) . "\n";
+}
+
+/**
+ * Normalizes a YouTube URL to include a v= parameter and a query string free of encoded ampersands.
+ *
+ * @param string $url
+ * @return string The normalized URL
+ */
+if ( ! function_exists( 'youtube_sanitize_url' ) ) :
+ function youtube_sanitize_url( $url ) {
+ $url = trim( $url, ' "' );
+ $url = trim( $url );
+ $url = str_replace( array( 'youtu.be/', '/v/', '#!v=', '&amp;', '&#038;', 'playlist' ), array( 'youtu.be/?v=', '/?v=', '?v=', '&', '&', 'videoseries' ), $url );
+
+ // Replace any extra question marks with ampersands - the result of a URL like "http://www.youtube.com/v/9FhMMmqzbD8?fs=1&hl=en_US" being passed in.
+ $query_string_start = strpos( $url, '?' );
+
+ if ( false !== $query_string_start ) {
+ $url = substr( $url, 0, $query_string_start + 1 ) . str_replace( '?', '&', substr( $url, $query_string_start + 1 ) );
+ }
+
+ return $url;
+ }
+endif;
+
+/*
+ * url can be:
+ * http://www.youtube.com/embed/videoseries?list=PL94269DA08231042B&amp;hl=en_US
+ * http://www.youtube.com/watch#!v=H2Ncxw1xfck
+ * http://www.youtube.com/watch?v=H2Ncxw1xfck
+ * http://www.youtube.com/watch?v=H2Ncxw1xfck&w=320&h=240&fmt=1&rel=0&showsearch=1&hd=0
+ * http://www.youtube.com/v/jF-kELmmvgA
+ * http://www.youtube.com/v/9FhMMmqzbD8?fs=1&hl=en_US
+ * http://youtu.be/Rrohlqeir5E
+ */
+
+/**
+ * Converts a YouTube URL into an embedded YouTube video.
+ */
+function youtube_id( $url ) {
+ if ( ! $id = jetpack_get_youtube_id( $url ) ) {
+ return '<!--YouTube Error: bad URL entered-->';
+ }
+
+ $url = youtube_sanitize_url( $url );
+ $url = parse_url( $url );
+
+ if ( ! isset( $url['query'] ) ) {
+ return false;
+ }
+
+ if ( isset( $url['fragment'] ) ) {
+ wp_parse_str( $url['fragment'], $fargs );
+ } else {
+ $fargs = array();
+ }
+ wp_parse_str( $url['query'], $qargs );
+
+ $qargs = array_merge( $fargs, $qargs );
+
+ // calculate the width and height, taking content_width into consideration
+ global $content_width;
+
+ $input_w = ( isset( $qargs['w'] ) && intval( $qargs['w'] ) ) ? intval( $qargs['w'] ) : 0;
+ $input_h = ( isset( $qargs['h'] ) && intval( $qargs['h'] ) ) ? intval( $qargs['h'] ) : 0;
+
+ // If we have $content_width, use it.
+ if ( ! empty( $content_width ) ) {
+ $default_width = $content_width;
+ } else {
+ // Otherwise get default width from the old, now deprecated embed_size_w option.
+ $default_width = get_option( 'embed_size_w' );
+ }
+
+ // If we don't know those 2 values use a hardcoded width.h
+ if ( empty( $default_width ) ) {
+ $default_width = 640;
+ }
+
+ if ( $input_w > 0 && $input_h > 0 ) {
+ $w = $input_w;
+ $h = $input_h;
+ } elseif ( 0 == $input_w && 0 == $input_h ) {
+ if ( isset( $qargs['fmt'] ) && intval( $qargs['fmt'] ) ) {
+ $w = ( ! empty( $content_width ) ? min( $content_width, 480 ) : 480 );
+ } else {
+ $w = ( ! empty( $content_width ) ? min( $content_width, $default_width ) : $default_width );
+ $h = ceil( ( $w / 16 ) * 9 );
+ }
+ } elseif ( $input_w > 0 ) {
+ $w = $input_w;
+ $h = ceil( ( $w / 16 ) * 9 );
+ } else {
+ if ( isset( $qargs['fmt'] ) && intval( $qargs['fmt'] ) ) {
+ $w = ( ! empty( $content_width ) ? min( $content_width, 480 ) : 480 );
+ } else {
+ $w = ( ! empty( $content_width ) ? min( $content_width, $default_width ) : $default_width );
+ $h = $input_h;
+ }
+ }
+
+ /**
+ * Filter the YouTube player width.
+ *
+ * @module shortcodes
+ *
+ * @since 1.1.0
+ *
+ * @param int $w Width of the YouTube player in pixels.
+ */
+ $w = (int) apply_filters( 'youtube_width', $w );
+
+ /**
+ * Filter the YouTube player height.
+ *
+ * @module shortcodes
+ *
+ * @since 1.1.0
+ *
+ * @param int $h Height of the YouTube player in pixels.
+ */
+ $h = (int) apply_filters( 'youtube_height', $h );
+
+ $rel = ( isset( $qargs['rel'] ) && 0 == $qargs['rel'] ) ? 0 : 1;
+ $search = ( isset( $qargs['showsearch'] ) && 1 == $qargs['showsearch'] ) ? 1 : 0;
+ $info = ( isset( $qargs['showinfo'] ) && 0 == $qargs['showinfo'] ) ? 0 : 1;
+ $iv = ( isset( $qargs['iv_load_policy'] ) && 3 == $qargs['iv_load_policy'] ) ? 3 : 1;
+
+ $fmt = ( isset( $qargs['fmt'] ) && intval( $qargs['fmt'] ) ) ? '&fmt=' . (int) $qargs['fmt'] : '';
+
+ if ( ! isset( $qargs['autohide'] ) || ( $qargs['autohide'] < 0 || 2 < $qargs['autohide'] ) ) {
+ $autohide = '&autohide=2';
+ } else {
+ $autohide = '&autohide=' . absint( $qargs['autohide'] );
+ }
+
+ $start = 0;
+ if ( isset( $qargs['start'] ) ) {
+ $start = intval( $qargs['start'] );
+ } elseif ( isset( $qargs['t'] ) ) {
+ $time_pieces = preg_split( '/(?<=\D)(?=\d+)/', $qargs['t'] );
+
+ foreach ( $time_pieces as $time_piece ) {
+ $int = (int) $time_piece;
+ switch ( substr( $time_piece, -1 ) ) {
+ case 'h':
+ $start += $int * 3600;
+ break;
+ case 'm':
+ $start += $int * 60;
+ break;
+ case 's':
+ $start += $int;
+ break;
+ }
+ }
+ }
+
+ $start = $start ? '&start=' . $start : '';
+ $end = ( isset( $qargs['end'] ) && intval( $qargs['end'] ) ) ? '&end=' . (int) $qargs['end'] : '';
+ $hd = ( isset( $qargs['hd'] ) && intval( $qargs['hd'] ) ) ? '&hd=' . (int) $qargs['hd'] : '';
+
+ $vq = ( isset( $qargs['vq'] ) && in_array( $qargs['vq'], array( 'hd720', 'hd1080' ) ) ) ? '&vq=' . $qargs['vq'] : '';
+
+ $cc = ( isset( $qargs['cc_load_policy'] ) ) ? '&cc_load_policy=1' : '';
+ $cc_lang = ( isset( $qargs['cc_lang_pref'] ) ) ? '&cc_lang_pref=' . preg_replace( '/[^_a-z0-9-]/i', '', $qargs['cc_lang_pref'] ) : '';
+
+ $wmode = ( isset( $qargs['wmode'] ) && in_array( strtolower( $qargs['wmode'] ), array( 'opaque', 'window', 'transparent' ) ) ) ? $qargs['wmode'] : 'transparent';
+
+ $theme = ( isset( $qargs['theme'] ) && in_array( strtolower( $qargs['theme'] ), array( 'dark', 'light' ) ) ) ? '&theme=' . $qargs['theme'] : '';
+
+ $autoplay = '';
+ /**
+ * Allow YouTube videos to start playing automatically.
+ *
+ * @module shortcodes
+ *
+ * @since 2.2.2
+ *
+ * @param bool false Enable autoplay for YouTube videos.
+ */
+ if ( apply_filters( 'jetpack_youtube_allow_autoplay', false ) && isset( $qargs['autoplay'] ) ) {
+ $autoplay = '&autoplay=' . (int) $qargs['autoplay'];
+ }
+
+ if ( ( isset( $url['path'] ) && '/videoseries' == $url['path'] ) || isset( $qargs['list'] ) ) {
+ $html = "<iframe class='youtube-player' type='text/html' width='$w' height='$h' src='" . esc_url( "https://www.youtube.com/embed/videoseries?list=$id&hl=en_US" ) . "' allowfullscreen='true' style='border:0;'></iframe>";
+ } else {
+ $html = "<iframe class='youtube-player' type='text/html' width='$w' height='$h' src='" . esc_url( "https://www.youtube.com/embed/$id?version=3&rel=$rel&fs=1$fmt$autohide&showsearch=$search&showinfo=$info&iv_load_policy=$iv$start$end$hd&wmode=$wmode$theme$autoplay{$cc}{$cc_lang}" ) . "' allowfullscreen='true' style='border:0;'></iframe>";
+ }
+
+ // Let's do some alignment wonder in a span, unless we're producing a feed
+ if ( ! is_feed() ) {
+ $alignmentcss = 'text-align:center;';
+ if ( isset( $qargs['align'] ) ) {
+ switch ( $qargs['align'] ) {
+ case 'left':
+ $alignmentcss = "float:left; width:{$w}px; height:{$h}px; margin-right:10px; margin-bottom: 10px;";
+ break;
+ case 'right':
+ $alignmentcss = "float:right; width:{$w}px; height:{$h}px; margin-left:10px; margin-bottom: 10px;";
+ break;
+ }
+ }
+
+ $html = sprintf(
+ '<span class="embed-youtube" style="%s display: block;">%s</span>',
+ esc_attr( $alignmentcss ),
+ $html
+ );
+
+ }
+
+ /**
+ * Filter the YouTube video HTML output.
+ *
+ * @module shortcodes
+ *
+ * @since 1.2.3
+ *
+ * @param string $html YouTube video HTML output.
+ */
+ $html = apply_filters( 'video_embed_html', $html );
+
+ return $html;
+}
+
+function youtube_shortcode( $atts ) {
+ return youtube_id( ( isset( $atts[0] ) ) ? ltrim( $atts[0], '=' ) : shortcode_new_to_old_params( $atts ) );
+}
+
+add_shortcode( 'youtube', 'youtube_shortcode' );
+
+/**
+ * For bare URLs on their own line of the form
+ * http://www.youtube.com/v/9FhMMmqzbD8?fs=1&hl=en_US
+ */
+function wpcom_youtube_embed_crazy_url( $matches, $attr, $url ) {
+ return youtube_id( $url );
+}
+
+function wpcom_youtube_embed_crazy_url_init() {
+ wp_embed_register_handler( 'wpcom_youtube_embed_crazy_url', '#https?://(?:www\.)?(?:youtube.com/(?:v/|playlist|watch[/\#?])|youtu\.be/).*#i', 'wpcom_youtube_embed_crazy_url' );
+}
+
+add_action( 'init', 'wpcom_youtube_embed_crazy_url_init' );
+
+/**
+ * Allow oEmbeds in Jetpack's Comment form.
+ *
+ * @module shortcodes
+ *
+ * @since 2.8.0
+ *
+ * @param int get_option('embed_autourls') Option to automatically embed all plain text URLs.
+ */
+if ( ! is_admin() && apply_filters( 'jetpack_comments_allow_oembed', true ) ) {
+ // We attach wp_kses_post to comment_text in default-filters.php with priority of 10 anyway, so the iframe gets filtered out.
+ // Higher priority because we need it before auto-link and autop get to it
+ add_filter( 'comment_text', 'youtube_link', 1 );
+}
+
+/**
+ * Core changes to do_shortcode (https://core.trac.wordpress.org/changeset/34747) broke "improper" shortcodes
+ * with the format [shortcode=http://url.com].
+ *
+ * This removes the "=" from the shortcode so it can be parsed.
+ *
+ * @see https://github.com/Automattic/jetpack/issues/3121
+ */
+function jetpack_fix_youtube_shortcode_display_filter( $content ) {
+ if ( strpos( $content, '[youtube=' ) !== false ) {
+ $content = preg_replace( '@\[youtube=(.*?)\]@', '[youtube $1]', $content );
+ }
+
+ return $content;
+}
+add_filter( 'the_content', 'jetpack_fix_youtube_shortcode_display_filter', 7 );
diff --git a/plugins/jetpack/modules/shortlinks.php b/plugins/jetpack/modules/shortlinks.php
new file mode 100644
index 00000000..a34ed3c8
--- /dev/null
+++ b/plugins/jetpack/modules/shortlinks.php
@@ -0,0 +1,140 @@
+<?php
+/**
+ * Module Name: WP.me Shortlinks
+ * Module Description: Generates shorter links so you can have more space to write on social media sites.
+ * Sort Order: 8
+ * First Introduced: 1.1
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Social
+ * Feature: Writing
+ * Additional Search Queries: shortlinks, wp.me
+ */
+
+add_filter( 'pre_get_shortlink', 'wpme_get_shortlink_handler', 1, 4 );
+
+if ( !function_exists( 'wpme_dec2sixtwo' ) ) {
+ function wpme_dec2sixtwo( $num ) {
+ $index = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ $out = "";
+
+ if ( $num < 0 ) {
+ $out = '-';
+ $num = abs( $num );
+ }
+
+ for ( $t = floor( log10( $num ) / log10( 62 ) ); $t >= 0; $t-- ) {
+ $a = floor( $num / pow( 62, $t ) );
+ $out = $out . substr( $index, $a, 1 );
+ $num = $num - ( $a * pow( 62, $t ) );
+ }
+
+ return $out;
+ }
+}
+
+function wpme_get_shortlink( $id = 0, $context = 'post', $allow_slugs = true ) {
+ global $wp_query;
+
+ $blog_id = Jetpack_Options::get_option( 'id' );
+
+ if ( 'query' == $context ) {
+ if ( is_singular() ) {
+ $id = $wp_query->get_queried_object_id();
+ $context = 'post';
+ } elseif ( is_front_page() ) {
+ $context = 'blog';
+ } else {
+ return '';
+ }
+ }
+
+ if ( 'blog' == $context ) {
+ if ( empty( $id ) )
+ $id = $blog_id;
+
+ return 'https://wp.me/' . wpme_dec2sixtwo( $id );
+ }
+
+ $post = get_post( $id );
+
+ if ( empty( $post ) )
+ return '';
+
+ $post_id = $post->ID;
+ $type = '';
+
+ if ( $allow_slugs && 'publish' == $post->post_status && 'post' == $post->post_type && strlen( $post->post_name ) <= 8 && false === strpos( $post->post_name, '%' )
+ && false === strpos( $post->post_name, '-' ) ) {
+ $id = $post->post_name;
+ $type = 's';
+ } else {
+ $id = wpme_dec2sixtwo( $post_id );
+ if ( 'page' == $post->post_type )
+ $type = 'P';
+ elseif ( 'post' == $post->post_type || post_type_supports( $post->post_type, 'shortlinks' ) )
+ $type= 'p';
+ elseif ( 'attachment' == $post->post_type )
+ $type = 'a';
+ }
+
+ if ( empty( $type ) )
+ return '';
+
+ return 'https://wp.me/' . $type . wpme_dec2sixtwo( $blog_id ) . '-' . $id;
+}
+
+function wpme_get_shortlink_handler( $shortlink, $id, $context, $allow_slugs ) {
+ return wpme_get_shortlink( $id, $context, $allow_slugs );
+}
+
+/**
+ * Add Shortlinks to the REST API responses.
+ *
+ * @since 6.9.0
+ *
+ * @action rest_api_init
+ * @uses register_rest_field, wpme_rest_get_shortlink
+ */
+function wpme_rest_register_shortlinks() {
+ register_rest_field(
+ array(
+ 'attachment',
+ 'page',
+ 'post',
+ ),
+ 'jetpack_shortlink',
+ array(
+ 'get_callback' => 'wpme_rest_get_shortlink',
+ 'update_callback' => null,
+ 'schema' => null,
+ )
+ );
+}
+
+/**
+ * Get the shortlink of a post.
+ *
+ * @since 6.9.0
+ *
+ * @param array $object Details of current post.
+ *
+ * @uses wpme_get_shortlink
+ *
+ * @return string
+ */
+function wpme_rest_get_shortlink( $object ) {
+ return wpme_get_shortlink( $object['id'], array() );
+}
+
+// Add shortlinks to the REST API Post response.
+add_action( 'rest_api_init', 'wpme_rest_register_shortlinks' );
+
+/**
+ * Set the Shortlink Gutenberg extension as available.
+ */
+function wpme_set_extension_available() {
+ Jetpack_Gutenberg::set_extension_available( 'jetpack/shortlinks' );
+}
+
+add_action( 'init', 'wpme_set_extension_available' );
diff --git a/plugins/jetpack/modules/simple-payments/paypal-express-checkout.js b/plugins/jetpack/modules/simple-payments/paypal-express-checkout.js
new file mode 100644
index 00000000..ad8cc71d
--- /dev/null
+++ b/plugins/jetpack/modules/simple-payments/paypal-express-checkout.js
@@ -0,0 +1,248 @@
+/**
+ * This PaypalExpressCheckout global is included by wp_enqueue_script( 'paypal-express-checkout' );
+ * It handles communication with Paypal Express checkout and public-api.wordpress.com for the purposes
+ * of simple-payments module.
+ */
+
+/* global paypal */
+/* global jQuery */
+/* exported PaypalExpressCheckout */
+/* jshint unused:false, es3:false, esversion:5 */
+var PaypalExpressCheckout = {
+ primaryCssClassName: 'jetpack-simple-payments',
+ messageCssClassName: 'jetpack-simple-payments-purchase-message',
+
+ wpRestAPIHost: 'https://public-api.wordpress.com',
+ wpRestAPIVersion: '/wpcom/v2',
+
+ getEnvironment: function() {
+ if (
+ localStorage &&
+ localStorage.getItem &&
+ localStorage.getItem( 'simple-payments-env' ) === 'sandbox'
+ ) {
+ return 'sandbox';
+ }
+ return 'production';
+ },
+
+ getCreatePaymentEndpoint: function( blogId ) {
+ return (
+ PaypalExpressCheckout.wpRestAPIHost +
+ PaypalExpressCheckout.wpRestAPIVersion +
+ '/sites/' +
+ blogId +
+ '/simple-payments/paypal/payment'
+ );
+ },
+
+ getExecutePaymentEndpoint: function( blogId, paymentId ) {
+ return (
+ PaypalExpressCheckout.wpRestAPIHost +
+ PaypalExpressCheckout.wpRestAPIVersion +
+ '/sites/' +
+ blogId +
+ '/simple-payments/paypal/' +
+ paymentId +
+ '/execute'
+ );
+ },
+
+ getNumberOfItems: function( field, enableMultiple ) {
+ if ( enableMultiple !== '1' ) {
+ return 1;
+ }
+
+ var numberField = document.getElementById( field );
+
+ if ( ! numberField ) {
+ return 1;
+ }
+
+ var number = Number( numberField.value );
+
+ if ( isNaN( number ) ) {
+ return 1;
+ }
+ return number;
+ },
+
+ /**
+ * Get the DOM element-placeholder used to show message
+ * about the transaction. If it doesn't exist then the function will create a new one.
+ *
+ * @param string domId id of the payment button placeholder
+ * @return Element the dom element to print the message
+ */
+ getMessageContainer: function( domId ) {
+ return document.getElementById( domId + '-message-container' );
+ },
+
+ /**
+ * Show a messange close to the Paypal button.
+ * Use this function to give feedback to the user according
+ * to the transaction result.
+ *
+ * @param {String} message message to show
+ * @param {String} domId paypal-button element dom identifier
+ * @param {Boolean} [error] defines if it's a message error. Not TRUE as default.
+ */
+ showMessage: function( message, domId, isError ) {
+ var domEl = PaypalExpressCheckout.getMessageContainer( domId );
+
+ // set css classes
+ var cssClasses = PaypalExpressCheckout.messageCssClassName + ' show ';
+ cssClasses += isError ? 'error' : 'success';
+
+ // show message 1s after PayPal popup is closed
+ setTimeout( function() {
+ domEl.innerHTML = message;
+ domEl.setAttribute( 'class', cssClasses );
+ }, 1000 );
+ },
+
+ showError: function( message, domId ) {
+ PaypalExpressCheckout.showMessage( message, domId, true );
+ },
+
+ processErrorMessage: function( errorResponse ) {
+ var error = errorResponse ? errorResponse.responseJSON : null;
+ var defaultMessage = 'There was an issue processing your payment.';
+
+ if ( ! error ) {
+ return '<p>' + defaultMessage + '</p>';
+ }
+
+ if ( error.additional_errors ) {
+ var messages = [];
+ error.additional_errors.forEach( function( additionalError ) {
+ if ( additionalError.message ) {
+ messages.push( '<p>' + additionalError.message.toString() + '</p>' );
+ }
+ } );
+ return messages.join( '' );
+ }
+
+ return '<p>' + ( error.message || defaultMessage ) + '</p>';
+ },
+
+ processSuccessMessage: function( successResponse ) {
+ var message = successResponse.message;
+ var defaultMessage = 'Thank you. Your purchase was successful!';
+
+ if ( ! message ) {
+ return '<p>' + defaultMessage + '</p>';
+ }
+
+ return '<p>' + message + '</p>';
+ },
+
+ cleanAndHideMessage: function( domId ) {
+ var domEl = PaypalExpressCheckout.getMessageContainer( domId );
+ domEl.setAttribute( 'class', PaypalExpressCheckout.messageCssClassName );
+ domEl.innerHTML = '';
+ },
+
+ renderButton: function( blogId, buttonId, domId, enableMultiple ) {
+ var env = PaypalExpressCheckout.getEnvironment();
+
+ if ( ! paypal ) {
+ throw new Error( 'PayPal module is required by PaypalExpressCheckout' );
+ }
+
+ var buttonDomId = domId + '_button';
+
+ paypal.Button.render(
+ {
+ env: env,
+ commit: true,
+
+ style: {
+ label: 'pay',
+ shape: 'rect',
+ color: 'silver',
+ fundingicons: true,
+ },
+
+ payment: function() {
+ PaypalExpressCheckout.cleanAndHideMessage( domId );
+
+ var payload = {
+ number: PaypalExpressCheckout.getNumberOfItems( domId + '_number', enableMultiple ),
+ buttonId: buttonId,
+ env: env,
+ };
+
+ return new paypal.Promise( function( resolve, reject ) {
+ jQuery
+ .post( PaypalExpressCheckout.getCreatePaymentEndpoint( blogId ), payload )
+ .done( function( paymentResponse ) {
+ if ( ! paymentResponse ) {
+ PaypalExpressCheckout.showError(
+ PaypalExpressCheckout.processErrorMessage(),
+ domId
+ );
+ return reject( new Error( 'server_error' ) );
+ }
+
+ resolve( paymentResponse.id );
+ } )
+ .fail( function( paymentError ) {
+ var paymentErrorMessage = PaypalExpressCheckout.processErrorMessage( paymentError );
+ PaypalExpressCheckout.showError( paymentErrorMessage, domId );
+
+ var code =
+ paymentError.responseJSON && paymentError.responseJSON.code
+ ? paymentError.responseJSON.code
+ : 'server_error';
+
+ reject( new Error( code ) );
+ } );
+ } );
+ },
+
+ onAuthorize: function( onAuthData ) {
+ var payload = {
+ buttonId: buttonId,
+ payerId: onAuthData.payerID,
+ env: env,
+ };
+ return new paypal.Promise( function( resolve, reject ) {
+ jQuery
+ .post(
+ PaypalExpressCheckout.getExecutePaymentEndpoint( blogId, onAuthData.paymentID ),
+ payload
+ )
+ .done( function( authResponse ) {
+ if ( ! authResponse ) {
+ PaypalExpressCheckout.showError(
+ PaypalExpressCheckout.processErrorMessage(),
+ domId
+ );
+ return reject( new Error( 'server_error' ) );
+ }
+
+ PaypalExpressCheckout.showMessage(
+ PaypalExpressCheckout.processSuccessMessage( authResponse ),
+ domId
+ );
+ resolve();
+ } )
+ .fail( function( authError ) {
+ var authErrorMessage = PaypalExpressCheckout.processErrorMessage( authError );
+ PaypalExpressCheckout.showError( authErrorMessage, domId );
+
+ var code =
+ authError.responseJSON && authError.responseJSON.code
+ ? authError.responseJSON.code
+ : 'server_error';
+
+ reject( new Error( code ) );
+ } );
+ } );
+ },
+ },
+ buttonDomId
+ );
+ },
+};
diff --git a/plugins/jetpack/modules/simple-payments/simple-payments.css b/plugins/jetpack/modules/simple-payments/simple-payments.css
new file mode 100644
index 00000000..dd479fc1
--- /dev/null
+++ b/plugins/jetpack/modules/simple-payments/simple-payments.css
@@ -0,0 +1,129 @@
+.jetpack-simple-payments-wrapper {
+ margin-bottom: 1.5em;
+}
+
+/* Higher specificity in order to reset paragraph style */
+body .jetpack-simple-payments-wrapper .jetpack-simple-payments-details p {
+ margin: 0 0 1.5em;
+ padding: 0;
+}
+
+.jetpack-simple-payments-product {
+ display: flex;
+ flex-direction: column;
+}
+
+.jetpack-simple-payments-product-image {
+ flex: 0 0 30%;
+ margin-bottom: 1.5em;
+}
+
+.jetpack-simple-payments-image {
+ box-sizing: border-box;
+ min-width: 70px;
+ padding-top: 100%;
+ position: relative;
+}
+
+/* Higher specificity in order to trump theme's style */
+body .jetpack-simple-payments-wrapper .jetpack-simple-payments-product-image .jetpack-simple-payments-image img.size-full {
+ border: 0;
+ border-radius: 0;
+ height: auto;
+ left: 50%;
+ margin: 0;
+ max-height: 100%;
+ max-width: 100%;
+ padding: 0;
+ position: absolute;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: auto;
+}
+
+.jetpack-simple-payments-title p,
+.jetpack-simple-payments-price p {
+ font-weight: bold;
+}
+
+.jetpack-simple-payments-purchase-box {
+ align-items: flex-start;
+ display: flex;
+}
+
+.jetpack-simple-payments-items {
+ flex: 0 0 auto;
+ margin-right: 10px;
+}
+
+input[type="number"].jetpack-simple-payments-items-number {
+ font-size: 16px;
+ line-height: 1;
+ max-width: 60px;
+ padding: 4px 8px;
+}
+
+.jetpack-simple-payments-button iframe {
+ margin: 0;
+}
+
+.jetpack-simple-payments-purchase-message {
+ background-color: rgba(255, 255, 255, 0.7);
+ border: 2px solid #fff;
+ border-radius: 2px;
+ box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
+ display: none;
+ margin-bottom: 1.5em;
+ min-height: 48px;
+ padding: 1em;
+ position: relative;
+}
+
+.jetpack-simple-payments-purchase-message:before {
+ font-family: dashicons !important;
+ font-size: 48px !important;
+ line-height: 1 !important;
+ position: absolute;
+ speak: none;
+ top: 50%;
+ left: 0;
+ transform: translateY(-50%);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.jetpack-simple-payments-purchase-message.show {
+ display: block;
+}
+
+.jetpack-simple-payments-purchase-message.success:before {
+ color: #4ab866;
+ content: "\f147";
+}
+
+.jetpack-simple-payments-purchase-message.error:before {
+ color: #d94f4f;
+ content: "\f335";
+}
+
+/* Higher specificity in order to reset */
+body .jetpack-simple-payments-wrapper .jetpack-simple-payments-purchase-message p {
+ color: #222;
+ margin: 0 0 0.5em;
+ padding: 0 0 0 40px;
+}
+
+body .jetpack-simple-payments-wrapper .jetpack-simple-payments-purchase-message p:last-child {
+ margin: 0;
+}
+
+@media screen and (min-width: 400px) {
+ .jetpack-simple-payments-product {
+ flex-direction: row;
+ }
+
+ .jetpack-simple-payments-product-image + .jetpack-simple-payments-details {
+ flex-basis: 70%;
+ padding-left: 1em;
+ }
+}
diff --git a/plugins/jetpack/modules/simple-payments/simple-payments.php b/plugins/jetpack/modules/simple-payments/simple-payments.php
new file mode 100644
index 00000000..e1b7f8d7
--- /dev/null
+++ b/plugins/jetpack/modules/simple-payments/simple-payments.php
@@ -0,0 +1,683 @@
+<?php
+/*
+ * Simple Payments lets users embed a PayPal button fully integrated with wpcom to sell products on the site.
+ * This is not a proper module yet, because not all the pieces are in place. Until everything is shipped, it can be turned
+ * into module that can be enabled/disabled.
+*/
+class Jetpack_Simple_Payments {
+ // These have to be under 20 chars because that is CPT limit.
+ static $post_type_order = 'jp_pay_order';
+ static $post_type_product = 'jp_pay_product';
+
+ static $shortcode = 'simple-payment';
+
+ static $css_classname_prefix = 'jetpack-simple-payments';
+
+ // Increase this number each time there's a change in CSS or JS to bust cache.
+ static $version = '0.25';
+
+ // Classic singleton pattern:
+ private static $instance;
+ private function __construct() {}
+ static function getInstance() {
+ if ( ! self::$instance ) {
+ self::$instance = new self();
+ self::$instance->register_init_hooks();
+ }
+ return self::$instance;
+ }
+
+ private function register_scripts_and_styles() {
+ /**
+ * Paypal heavily discourages putting that script in your own server:
+ * @see https://developer.paypal.com/docs/integration/direct/express-checkout/integration-jsv4/add-paypal-button/
+ */
+ wp_register_script( 'paypal-checkout-js', 'https://www.paypalobjects.com/api/checkout.js', array(), null, true );
+ wp_register_script( 'paypal-express-checkout', plugins_url( '/paypal-express-checkout.js', __FILE__ ),
+ array( 'jquery', 'paypal-checkout-js' ), self::$version );
+ wp_register_style( 'jetpack-simple-payments', plugins_url( '/simple-payments.css', __FILE__ ), array( 'dashicons' ) );
+ }
+
+ private function register_init_hooks() {
+ add_action( 'init', array( $this, 'init_hook_action' ) );
+ add_action( 'jetpack_register_gutenberg_extensions', array( $this, 'register_gutenberg_block' ) );
+ add_action( 'rest_api_init', array( $this, 'register_meta_fields_in_rest_api' ) );
+ }
+
+ private function register_shortcode() {
+ add_shortcode( self::$shortcode, array( $this, 'parse_shortcode' ) );
+ }
+
+ public function init_hook_action() {
+ add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
+ add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
+ if ( ! is_admin() ) {
+ $this->register_scripts_and_styles();
+ }
+ $this->register_shortcode();
+ $this->setup_cpts();
+
+ add_filter( 'the_content', array( $this, 'remove_auto_paragraph_from_product_description' ), 0 );
+ }
+
+ function register_gutenberg_block() {
+ if ( $this->is_enabled_jetpack_simple_payments() ) {
+ jetpack_register_block( 'jetpack/simple-payments' );
+ } else {
+ Jetpack_Gutenberg::set_extension_unavailable( 'jetpack/simple-payments', 'missing_plan' );
+ }
+ }
+
+ function remove_auto_paragraph_from_product_description( $content ) {
+ if ( get_post_type() === self::$post_type_product ) {
+ remove_filter( 'the_content', 'wpautop' );
+ }
+
+ return $content;
+ }
+
+ function get_blog_id() {
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ return get_current_blog_id();
+ }
+
+ return Jetpack_Options::get_option( 'id' );
+ }
+
+ /**
+ * Used to check whether Simple Payments are enabled for given site.
+ *
+ * @return bool True if Simple Payments are enabled, false otherwise.
+ */
+ function is_enabled_jetpack_simple_payments() {
+ /**
+ * Can be used by plugin authors to disable the conflicting output of Simple Payments.
+ *
+ * @since 6.3.0
+ *
+ * @param bool True if Simple Payments should be disabled, false otherwise.
+ */
+ if ( apply_filters( 'jetpack_disable_simple_payments', false ) ) {
+ return false;
+ }
+
+ // For WPCOM sites
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'has_any_blog_stickers' ) ) {
+ $site_id = $this->get_blog_id();
+ return has_any_blog_stickers( array( 'premium-plan', 'business-plan', 'ecommerce-plan' ), $site_id );
+ }
+
+ // For all Jetpack sites
+ return Jetpack::is_active() && Jetpack_Plan::supports( 'simple-payments');
+ }
+
+ function parse_shortcode( $attrs, $content = false ) {
+ if ( empty( $attrs['id'] ) ) {
+ return;
+ }
+ $product = get_post( $attrs['id'] );
+ if ( ! $product || is_wp_error( $product ) ) {
+ return;
+ }
+ if ( $product->post_type !== self::$post_type_product || 'publish' !== $product->post_status ) {
+ return;
+ }
+
+ // We allow for overriding the presentation labels
+ $data = shortcode_atts( array(
+ 'blog_id' => $this->get_blog_id(),
+ 'dom_id' => uniqid( self::$css_classname_prefix . '-' . $product->ID . '_', true ),
+ 'class' => self::$css_classname_prefix . '-' . $product->ID,
+ 'title' => get_the_title( $product ),
+ 'description' => $product->post_content,
+ 'cta' => get_post_meta( $product->ID, 'spay_cta', true ),
+ 'multiple' => get_post_meta( $product->ID, 'spay_multiple', true ) || '0'
+ ), $attrs );
+
+ $data['price'] = $this->format_price(
+ get_post_meta( $product->ID, 'spay_price', true ),
+ get_post_meta( $product->ID, 'spay_currency', true )
+ );
+
+ $data['id'] = $attrs['id'];
+
+ if( ! wp_style_is( 'jetpack-simple-payments', 'enqueued' ) ) {
+ wp_enqueue_style( 'jetpack-simple-payments' );
+ }
+
+ if ( ! $this->is_enabled_jetpack_simple_payments() ) {
+ return $this->output_admin_warning( $data );
+ }
+
+ if ( ! wp_script_is( 'paypal-express-checkout', 'enqueued' ) ) {
+ wp_enqueue_script( 'paypal-express-checkout' );
+ }
+
+ wp_add_inline_script( 'paypal-express-checkout', sprintf(
+ "try{PaypalExpressCheckout.renderButton( '%d', '%d', '%s', '%d' );}catch(e){}",
+ esc_js( $data['blog_id'] ),
+ esc_js( $attrs['id'] ),
+ esc_js( $data['dom_id'] ),
+ esc_js( $data['multiple'] )
+ ) );
+
+ return $this->output_shortcode( $data );
+ }
+
+ function output_admin_warning( $data ) {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+ $css_prefix = self::$css_classname_prefix;
+
+ $support_url = ( defined( 'IS_WPCOM' ) && IS_WPCOM )
+ ? 'https://support.wordpress.com/simple-payments/'
+ : 'https://jetpack.com/support/simple-payment-button/';
+
+ return sprintf( '
+<div class="%1$s">
+ <div class="%2$s">
+ <div class="%3$s">
+ <div class="%4$s" id="%5$s">
+ <p>%6$s</p>
+ <p>%7$s</p>
+ </div>
+ </div>
+ </div>
+</div>
+',
+ esc_attr( "{$data['class']} ${css_prefix}-wrapper" ),
+ esc_attr( "${css_prefix}-product" ),
+ esc_attr( "${css_prefix}-details" ),
+ esc_attr( "${css_prefix}-purchase-message show error" ),
+ esc_attr( "{$data['dom_id']}-message-container" ),
+ sprintf(
+ wp_kses(
+ __( 'Your plan doesn\'t include Simple Payments. <a href="%s" rel="noopener noreferrer" target="_blank">Learn more and upgrade</a>.', 'jetpack' ),
+ array( 'a' => array( 'href' => array(), 'rel' => array(), 'target' => array() ) )
+ ),
+ esc_url( $support_url )
+ ),
+ esc_html__( '(Only administrators will see this message.)', 'jetpack' )
+ );
+ }
+
+ function output_shortcode( $data ) {
+ $items = '';
+ $css_prefix = self::$css_classname_prefix;
+
+ if ( $data['multiple'] ) {
+ $items = sprintf( '
+ <div class="%1$s">
+ <input class="%2$s" type="number" value="1" min="1" id="%3$s" />
+ </div>
+ ',
+ esc_attr( "${css_prefix}-items" ),
+ esc_attr( "${css_prefix}-items-number" ),
+ esc_attr( "{$data['dom_id']}_number" )
+ );
+ }
+ $image = "";
+ if( has_post_thumbnail( $data['id'] ) ) {
+ $image = sprintf( '<div class="%1$s"><div class="%2$s">%3$s</div></div>',
+ esc_attr( "${css_prefix}-product-image" ),
+ esc_attr( "${css_prefix}-image" ),
+ get_the_post_thumbnail( $data['id'], 'full' )
+ );
+ }
+ return sprintf( '
+<div class="%1$s">
+ <div class="%2$s">
+ %3$s
+ <div class="%4$s">
+ <div class="%5$s"><p>%6$s</p></div>
+ <div class="%7$s"><p>%8$s</p></div>
+ <div class="%9$s"><p>%10$s</p></div>
+ <div class="%11$s" id="%12$s"></div>
+ <div class="%13$s">
+ %14$s
+ <div class="%15$s" id="%16$s"></div>
+ </div>
+ </div>
+ </div>
+</div>
+',
+ esc_attr( "{$data['class']} ${css_prefix}-wrapper" ),
+ esc_attr( "${css_prefix}-product" ),
+ $image,
+ esc_attr( "${css_prefix}-details" ),
+ esc_attr( "${css_prefix}-title" ),
+ $data['title'],
+ esc_attr( "${css_prefix}-description" ),
+ $data['description'],
+ esc_attr( "${css_prefix}-price" ),
+ esc_html( $data['price'] ),
+ esc_attr( "${css_prefix}-purchase-message" ),
+ esc_attr( "{$data['dom_id']}-message-container" ),
+ esc_attr( "${css_prefix}-purchase-box" ),
+ $items,
+ esc_attr( "${css_prefix}-button" ),
+ esc_attr( "{$data['dom_id']}_button" )
+ );
+ }
+
+ /**
+ * Format a price with currency
+ *
+ * Uses currency-aware formatting to output a formatted price with a simple fallback.
+ *
+ * Largely inspired by WordPress.com's Store_Price::display_currency
+ *
+ * @param string $price Price.
+ * @param string $currency Currency.
+ * @return string Formatted price.
+ */
+ private function format_price( $price, $currency ) {
+ $currency_details = self::get_currency( $currency );
+
+ if ( $currency_details ) {
+ // Ensure USD displays as 1234.56 even in non-US locales.
+ $amount = 'USD' === $currency
+ ? number_format( $price, $currency_details['decimal'], '.', ',' )
+ : number_format_i18n( $price, $currency_details['decimal'] );
+
+ return sprintf(
+ $currency_details['format'],
+ $currency_details['symbol'],
+ $amount
+ );
+ }
+
+ // Fall back to unspecified currency symbol like `¤1,234.05`.
+ // @link https://en.wikipedia.org/wiki/Currency_sign_(typography).
+ if ( ! $currency ) {
+ return '¤' . number_format_i18n( $price, 2 );
+ }
+
+ return number_format_i18n( $price, 2 ) . ' ' . $currency;
+ }
+
+ /**
+ * Allows custom post types to be used by REST API.
+ * @param $post_types
+ * @see hook 'rest_api_allowed_post_types'
+ * @return array
+ */
+ function allow_rest_api_types( $post_types ) {
+ $post_types[] = self::$post_type_order;
+ $post_types[] = self::$post_type_product;
+ return $post_types;
+ }
+
+ function allow_sync_post_meta( $post_meta ) {
+ return array_merge( $post_meta, array(
+ 'spay_paypal_id',
+ 'spay_status',
+ 'spay_product_id',
+ 'spay_quantity',
+ 'spay_price',
+ 'spay_customer_email',
+ 'spay_currency',
+ 'spay_cta',
+ 'spay_email',
+ 'spay_multiple',
+ 'spay_formatted_price',
+ ) );
+ }
+
+ /**
+ * Enable Simple payments custom meta values for access through the REST API.
+ * Field’s value will be exposed on a .meta key in the endpoint response,
+ * and WordPress will handle setting up the callbacks for reading and writing
+ * to that meta key.
+ *
+ * @link https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-responses/
+ */
+ public function register_meta_fields_in_rest_api() {
+ register_meta( 'post', 'spay_price', array(
+ 'description' => esc_html__( 'Simple payments; price.', 'jetpack' ),
+ 'object_subtype' => self::$post_type_product,
+ 'sanitize_callback' => array( $this, 'sanitize_price' ),
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'number',
+ ) );
+
+ register_meta( 'post', 'spay_currency', array(
+ 'description' => esc_html__( 'Simple payments; currency code.', 'jetpack' ),
+ 'object_subtype' => self::$post_type_product,
+ 'sanitize_callback' => array( $this, 'sanitize_currency' ),
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ ) );
+
+ register_meta( 'post', 'spay_cta', array(
+ 'description' => esc_html__( 'Simple payments; text with "Buy" or other CTA', 'jetpack' ),
+ 'object_subtype' => self::$post_type_product,
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ ) );
+
+ register_meta( 'post', 'spay_multiple', array(
+ 'description' => esc_html__( 'Simple payments; allow multiple items', 'jetpack' ),
+ 'object_subtype' => self::$post_type_product,
+ 'sanitize_callback' => 'rest_sanitize_boolean',
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'boolean',
+ ) );
+
+ register_meta( 'post', 'spay_email', array(
+ 'description' => esc_html__( 'Simple payments button; paypal email.', 'jetpack' ),
+ 'sanitize_callback' => 'sanitize_email',
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ ) );
+
+ register_meta( 'post', 'spay_status', array(
+ 'description' => esc_html__( 'Simple payments; status.', 'jetpack' ),
+ 'object_subtype' => self::$post_type_product,
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ ) );
+ }
+
+ /**
+ * Sanitize three-character ISO-4217 Simple payments currency
+ *
+ * List has to be in sync with list at the client side:
+ * @link https://github.com/Automattic/wp-calypso/blob/6d02ffe73cc073dea7270a22dc30881bff17d8fb/client/lib/simple-payments/constants.js
+ *
+ * Currencies should be supported by PayPal:
+ * @link https://developer.paypal.com/docs/integration/direct/rest/currency-codes/
+ */
+ public static function sanitize_currency( $currency ) {
+ $valid_currencies = array(
+ 'USD',
+ 'EUR',
+ 'AUD',
+ 'BRL',
+ 'CAD',
+ 'CZK',
+ 'DKK',
+ 'HKD',
+ 'HUF',
+ 'ILS',
+ 'JPY',
+ 'MYR',
+ 'MXN',
+ 'TWD',
+ 'NZD',
+ 'NOK',
+ 'PHP',
+ 'PLN',
+ 'GBP',
+ 'RUB',
+ 'SGD',
+ 'SEK',
+ 'CHF',
+ 'THB',
+ );
+
+ return in_array( $currency, $valid_currencies ) ? $currency : false;
+ }
+
+ /**
+ * Sanitize price:
+ *
+ * Positive integers and floats
+ * Supports two decimal places.
+ * Maximum length: 10.
+ *
+ * See `price` from PayPal docs:
+ * @link https://developer.paypal.com/docs/api/orders/v1/#definition-item
+ *
+ * @param $value
+ * @return null|string
+ */
+ public static function sanitize_price( $price ) {
+ return preg_match( '/^[0-9]{0,10}(\.[0-9]{0,2})?$/', $price ) ? $price : false;
+ }
+
+ /**
+ * Sets up the custom post types for the module.
+ */
+ function setup_cpts() {
+
+ /*
+ * ORDER data structure. holds:
+ * title = customer_name | 4xproduct_name
+ * excerpt = customer_name + customer contact info + customer notes from paypal form
+ * metadata:
+ * spay_paypal_id - paypal id of transaction
+ * spay_status
+ * spay_product_id - post_id of bought product
+ * spay_quantity - quantity of product
+ * spay_price - item price at the time of purchase
+ * spay_customer_email - customer email
+ * ... (WIP)
+ */
+ $order_capabilities = array(
+ 'edit_post' => 'edit_posts',
+ 'read_post' => 'read_private_posts',
+ 'delete_post' => 'delete_posts',
+ 'edit_posts' => 'edit_posts',
+ 'edit_others_posts' => 'edit_others_posts',
+ 'publish_posts' => 'publish_posts',
+ 'read_private_posts' => 'read_private_posts',
+ );
+ $order_args = array(
+ 'label' => esc_html_x( 'Order', 'noun: a quantity of goods or items purchased or sold', 'jetpack' ),
+ 'description' => esc_html__( 'Simple Payments orders', 'jetpack' ),
+ 'supports' => array( 'custom-fields', 'excerpt' ),
+ 'hierarchical' => false,
+ 'public' => false,
+ 'show_ui' => false,
+ 'show_in_menu' => false,
+ 'show_in_admin_bar' => false,
+ 'show_in_nav_menus' => false,
+ 'can_export' => true,
+ 'has_archive' => false,
+ 'exclude_from_search' => true,
+ 'publicly_queryable' => false,
+ 'rewrite' => false,
+ 'capabilities' => $order_capabilities,
+ 'show_in_rest' => true,
+ );
+ register_post_type( self::$post_type_order, $order_args );
+
+ /*
+ * PRODUCT data structure. Holds:
+ * title - title
+ * content - description
+ * thumbnail - image
+ * metadata:
+ * spay_price - price
+ * spay_formatted_price
+ * spay_currency - currency code
+ * spay_cta - text with "Buy" or other CTA
+ * spay_email - paypal email
+ * spay_multiple - allow for multiple items
+ * spay_status - status. { enabled | disabled }
+ */
+ $product_capabilities = array(
+ 'edit_post' => 'edit_posts',
+ 'read_post' => 'read_private_posts',
+ 'delete_post' => 'delete_posts',
+ 'edit_posts' => 'publish_posts',
+ 'edit_others_posts' => 'edit_others_posts',
+ 'publish_posts' => 'publish_posts',
+ 'read_private_posts' => 'read_private_posts',
+ );
+ $product_args = array(
+ 'label' => esc_html__( 'Product', 'jetpack' ),
+ 'description' => esc_html__( 'Simple Payments products', 'jetpack' ),
+ 'supports' => array( 'title', 'editor','thumbnail', 'custom-fields', 'author' ),
+ 'hierarchical' => false,
+ 'public' => false,
+ 'show_ui' => false,
+ 'show_in_menu' => false,
+ 'show_in_admin_bar' => false,
+ 'show_in_nav_menus' => false,
+ 'can_export' => true,
+ 'has_archive' => false,
+ 'exclude_from_search' => true,
+ 'publicly_queryable' => false,
+ 'rewrite' => false,
+ 'capabilities' => $product_capabilities,
+ 'show_in_rest' => true,
+ );
+ register_post_type( self::$post_type_product, $product_args );
+ }
+
+ /**
+ * Format a price for display
+ *
+ * Largely taken from WordPress.com Store_Price class
+ *
+ * The currency array will have the shape:
+ * format => string sprintf format with placeholders `%1$s`: Symbol `%2$s`: Price.
+ * symbol => string Symbol string
+ * desc => string Text description of currency
+ * decimal => int Number of decimal places
+ *
+ * @param string $the_currency The desired currency, e.g. 'USD'.
+ * @return ?array Currency object or null if not found.
+ */
+ private static function get_currency( $the_currency ) {
+ $currencies = array(
+ 'USD' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => '$',
+ 'decimal' => 2,
+ ),
+ 'GBP' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => '&#163;',
+ 'decimal' => 2,
+ ),
+ 'JPY' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => '&#165;',
+ 'decimal' => 0,
+ ),
+ 'BRL' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => 'R$',
+ 'decimal' => 2,
+ ),
+ 'EUR' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => '&#8364;',
+ 'decimal' => 2,
+ ),
+ 'NZD' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => 'NZ$',
+ 'decimal' => 2,
+ ),
+ 'AUD' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => 'A$',
+ 'decimal' => 2,
+ ),
+ 'CAD' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => 'C$',
+ 'decimal' => 2,
+ ),
+ 'ILS' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => '₪',
+ 'decimal' => 2,
+ ),
+ 'RUB' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => '₽',
+ 'decimal' => 2,
+ ),
+ 'MXN' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => 'MX$',
+ 'decimal' => 2,
+ ),
+ 'MYR' => array(
+ 'format' => '%2$s%1$s', // 1: Symbol 2: currency value
+ 'symbol' => 'RM',
+ 'decimal' => 2,
+ ),
+ 'SEK' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => 'Skr',
+ 'decimal' => 2,
+ ),
+ 'HUF' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => 'Ft',
+ 'decimal' => 0, // Decimals are supported by Stripe but not by PayPal.
+ ),
+ 'CHF' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => 'CHF',
+ 'decimal' => 2,
+ ),
+ 'CZK' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => 'Kč',
+ 'decimal' => 2,
+ ),
+ 'DKK' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => 'Dkr',
+ 'decimal' => 2,
+ ),
+ 'HKD' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => 'HK$',
+ 'decimal' => 2,
+ ),
+ 'NOK' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => 'Kr',
+ 'decimal' => 2,
+ ),
+ 'PHP' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => '₱',
+ 'decimal' => 2,
+ ),
+ 'PLN' => array(
+ 'format' => '%2$s %1$s', // 1: Symbol 2: currency value
+ 'symbol' => 'PLN',
+ 'decimal' => 2,
+ ),
+ 'SGD' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => 'S$',
+ 'decimal' => 2,
+ ),
+ 'TWD' => array(
+ 'format' => '%1$s%2$s', // 1: Symbol 2: currency value
+ 'symbol' => 'NT$',
+ 'decimal' => 0, // Decimals are supported by Stripe but not by PayPal.
+ ),
+ 'THB' => array(
+ 'format' => '%2$s%1$s', // 1: Symbol 2: currency value
+ 'symbol' => '฿',
+ 'decimal' => 2,
+ ),
+ );
+
+ if ( isset( $currencies[ $the_currency ] ) ) {
+ return $currencies[ $the_currency ];
+ }
+ return null;
+ }
+}
+Jetpack_Simple_Payments::getInstance();
diff --git a/plugins/jetpack/modules/site-icon.php b/plugins/jetpack/modules/site-icon.php
new file mode 100644
index 00000000..af374467
--- /dev/null
+++ b/plugins/jetpack/modules/site-icon.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer needed. Site Icons are in Core.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/site-icon/site-icon-functions.php b/plugins/jetpack/modules/site-icon/site-icon-functions.php
new file mode 100644
index 00000000..7adaf6fa
--- /dev/null
+++ b/plugins/jetpack/modules/site-icon/site-icon-functions.php
@@ -0,0 +1,29 @@
+<?php
+
+if( ! function_exists( 'jetpack_site_icon_url' ) ) :
+ function jetpack_site_icon_url( $blog_id = null, $size = '512', $default = false ) {
+ $url = '';
+ if( ! is_int( $blog_id ) )
+ $blog_id = get_current_blog_id();
+ if( function_exists( 'get_blog_option' ) ) {
+ $site_icon_id = get_blog_option( $blog_id, 'jetpack_site_icon_id' );
+ } else {
+ $site_icon_id = Jetpack_Options::get_option( 'site_icon_id' );
+ }
+ if( ! $site_icon_id ) {
+ if( $default === false && defined( 'SITE_ICON_DEFAULT_URL' ) )
+ $url = SITE_ICON_DEFAULT_URL;
+ else
+ $url = $default;
+ } else {
+ if( $size >= 512 ) {
+ $size_data = 'full';
+ } else {
+ $size_data = array( $size, $size );
+ }
+ $url_data = wp_get_attachment_image_src( $site_icon_id, $size_data );
+ $url = $url_data[0];
+ }
+ return $url;
+ }
+endif;
diff --git a/plugins/jetpack/modules/sitemaps.php b/plugins/jetpack/modules/sitemaps.php
new file mode 100644
index 00000000..79b2d08e
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Module Name: Sitemaps
+ * Module Description: Make it easy for search engines to find your site.
+ * Sort Order: 13
+ * First Introduced: 3.9
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Recommended, Traffic
+ * Feature: Recommended
+ * Additional Search Queries: sitemap, traffic, search, site map, seo
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Disable direct access and execution.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+if ( '1' == get_option( 'blog_public' ) ) { // loose comparison okay.
+ include_once 'sitemaps/sitemaps.php';
+}
+
+add_action( 'jetpack_activate_module_sitemaps', 'jetpack_sitemap_on_activate' );
+
+/**
+ * Run when Sitemaps module is activated.
+ *
+ * @since 4.8.0
+ */
+function jetpack_sitemap_on_activate() {
+ wp_clear_scheduled_hook( 'jp_sitemap_cron_hook' );
+ require_once dirname( __FILE__ ) . '/sitemaps/sitemap-constants.php';
+ require_once dirname( __FILE__ ) . '/sitemaps/sitemap-buffer.php';
+ require_once dirname( __FILE__ ) . '/sitemaps/sitemap-stylist.php';
+ require_once dirname( __FILE__ ) . '/sitemaps/sitemap-librarian.php';
+ require_once dirname( __FILE__ ) . '/sitemaps/sitemap-finder.php';
+ require_once dirname( __FILE__ ) . '/sitemaps/sitemap-builder.php';
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-fallback.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-fallback.php
new file mode 100644
index 00000000..46758766
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-fallback.php
@@ -0,0 +1,146 @@
+<?php
+/**
+ * The fallback buffer for users with no XML support.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing master sitemap xml files.
+ *
+ * @since 5.1.0
+ */
+abstract class Jetpack_Sitemap_Buffer_Fallback extends Jetpack_Sitemap_Buffer {
+
+ /**
+ * The buffer contents.
+ *
+ * @access protected
+ * @since 5.3.0
+ * @var string The buffer contents.
+ */
+ protected $buffer;
+
+ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) {
+ $this->is_full_flag = false;
+ $this->is_empty_flag = true;
+ $this->timestamp = $time;
+
+ $this->finder = new Jetpack_Sitemap_Finder();
+
+ $this->item_capacity = max( 1, intval( $item_limit ) );
+ $this->byte_capacity = max( 1, intval( $byte_limit ) ) - strlen( $this->contents() );
+ }
+
+ /**
+ * Append an item to the buffer, if there is room for it,
+ * and set is_empty_flag to false. If there is no room,
+ * we set is_full_flag to true. If $item is null,
+ * don't do anything and report success.
+ *
+ * @since 5.3.0
+ *
+ * @param array $array The item to be added.
+ *
+ * @return bool True if the append succeeded, False if not.
+ */
+ public function append( $array ) {
+ if ( is_null( $array ) ) {
+ return true;
+ }
+
+ if ( $this->is_full_flag ) {
+ return false;
+ }
+
+ if ( 0 >= $this->item_capacity || 0 >= $this->byte_capacity ) {
+ $this->is_full_flag = true;
+ return false;
+ } else {
+ $this->item_capacity -= 1;
+ $added_string = $this->array_to_xml_string( $array );
+ $this->buffer .= $added_string;
+ $this->is_empty_flag = false;
+
+ mbstring_binary_safe_encoding(); // So we can safely use strlen().
+ $this->byte_capacity -= strlen( $added_string );
+ reset_mbstring_encoding();
+
+ return true;
+ }
+ }
+
+ /**
+ * Detect whether the buffer is empty.
+ *
+ * @since 5.3.0
+ *
+ * @return bool True if the buffer is empty, false otherwise.
+ */
+ public function is_empty() {
+ return $this->is_empty_flag;
+ }
+
+ /**
+ * Retrieve the contents of the buffer.
+ *
+ * @since 5.3.0
+ *
+ * @return string The contents of the buffer (with the footer included).
+ */
+ public function contents() {
+ $root = $this->get_root_element();
+
+ return '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL . $root[0] . $this->buffer . $root[1] . PHP_EOL;
+ }
+
+ /**
+ * Legacy implementation of array to XML conversion without using DOMDocument.
+ *
+ * @param Array $array
+ * @return String $result
+ */
+ public function array_to_xml_string( $array, $parent = null, $root = null ) {
+ $string = '';
+
+ foreach ( $array as $key => $value ) {
+ // Only allow a-z, A-Z, colon, underscore, and hyphen.
+ $tag = preg_replace( '/[^a-zA-Z:_-]/', '_', $key );
+
+ if ( is_array( $value ) ) {
+ $string .= "<$tag>";
+ $string .= $this->array_to_xml_string( $value );
+ $string .= "</$tag>";
+ } elseif ( is_null( $value ) ) {
+ $string .= "<$tag />";
+ } else {
+ $string .= "<$tag>" . htmlspecialchars( $value ) . "</$tag>";
+ }
+ }
+
+ return $string;
+ }
+
+ /**
+ * Render an associative array of XML attribute key/value pairs.
+ *
+ * @access public
+ * @since 5.3.0
+ *
+ * @param array $array Key/value array of attributes.
+ *
+ * @return string The rendered attribute string.
+ */
+ public static function array_to_xml_attr_string( $array ) {
+ $string = '';
+
+ foreach ( $array as $key => $value ) {
+ $key = preg_replace( '/[^a-zA-Z:_-]/', '_', $key );
+ $string .= ' ' . $key . '="' . esc_attr( $value ) . '"';
+ }
+
+ return $string;
+ }
+
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-image-fallback.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-image-fallback.php
new file mode 100644
index 00000000..33628711
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-image-fallback.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Image
+ * extends the Jetpack_Sitemap_Buffer class to represent the single image sitemap
+ * buffer.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing sitemap image xml files for users that have no libxml support.
+ *
+ * @since 5.3.0
+ */
+class Jetpack_Sitemap_Buffer_Image extends Jetpack_Sitemap_Buffer_Fallback {
+
+ protected function get_root_element() {
+ if ( ! isset( $this->root ) ) {
+
+ /**
+ * Filter the XML namespaces included in image sitemaps.
+ *
+ * @module sitemaps
+ *
+ * @since 4.8.0
+ *
+ * @param array $namespaces Associative array with namespaces and namespace URIs.
+ */
+ $namespaces = apply_filters(
+ 'jetpack_sitemap_image_ns',
+ array(
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
+ 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
+ 'xmlns:image' => 'http://www.google.com/schemas/sitemap-image/1.1',
+ )
+ );
+
+ $sitemap_xsl_url = $this->finder->construct_sitemap_url( 'sitemap.xsl' );
+ $jetpack_version = JETPACK__VERSION;
+
+ $this->root = array(
+ "<!-- generator='jetpack-{$jetpack_version}' -->" . PHP_EOL
+ . "<?xml-stylesheet type='text/xsl' href='{$sitemap_xsl_url}'?>" . PHP_EOL
+ . '<urlset ' . $this->array_to_xml_attr_string( $namespaces ) . '>' . PHP_EOL,
+ '</urlset>',
+ );
+
+ $this->byte_capacity -= strlen( join( '', $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-image.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-image.php
new file mode 100644
index 00000000..e452fcbf
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-image.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Image
+ * extends the Jetpack_Sitemap_Buffer class to represent the single image sitemap
+ * buffer.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing sitemap image xml files.
+ *
+ * @since 5.3.0
+ */
+class Jetpack_Sitemap_Buffer_Image extends Jetpack_Sitemap_Buffer {
+
+ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) {
+ parent::__construct( $item_limit, $byte_limit, $time );
+
+ $this->doc->appendChild(
+ $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" )
+ );
+
+ $this->doc->appendChild(
+ $this->doc->createProcessingInstruction(
+ 'xml-stylesheet',
+ 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'image-sitemap.xsl' ) . '"'
+ )
+ );
+ }
+
+ protected function get_root_element() {
+ if ( ! isset( $this->root ) ) {
+
+ /**
+ * Filter the XML namespaces included in image sitemaps.
+ *
+ * @module sitemaps
+ *
+ * @since 4.8.0
+ *
+ * @param array $namespaces Associative array with namespaces and namespace URIs.
+ */
+ $namespaces = apply_filters(
+ 'jetpack_sitemap_image_ns',
+ array(
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
+ 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
+ 'xmlns:image' => 'http://www.google.com/schemas/sitemap-image/1.1',
+ )
+ );
+
+ $this->root = $this->doc->createElement( 'urlset' );
+
+ foreach ( $namespaces as $name => $value ) {
+ $this->root->setAttribute( $name, $value );
+ }
+
+ $this->doc->appendChild( $this->root );
+ $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-master-fallback.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-master-fallback.php
new file mode 100644
index 00000000..b63c5328
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-master-fallback.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Master
+ * extends the Jetpack_Sitemap_Buffer class to represent the master sitemap
+ * buffer.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing master sitemap xml files for users without libxml support.
+ *
+ * @since 5.3.0
+ */
+class Jetpack_Sitemap_Buffer_Master extends Jetpack_Sitemap_Buffer_Fallback {
+
+ protected function get_root_element() {
+
+ if ( ! isset( $this->root ) ) {
+
+ $sitemap_index_xsl_url = $this->finder->construct_sitemap_url( 'sitemap-index.xsl' );
+ $jetpack_version = JETPACK__VERSION;
+
+ $this->root = array(
+ "<!-- generator='jetpack-{$jetpack_version}' -->" . PHP_EOL
+ . "<?xml-stylesheet type='text/xsl' href='{$sitemap_index_xsl_url}'?>" . PHP_EOL
+ . "<sitemapindex xmlns='http://www.sitemaps.org/schemas/sitemap/0.9'>" . PHP_EOL,
+ '</sitemapindex>',
+ );
+
+ $this->byte_capacity -= strlen( join( '', $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-master.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-master.php
new file mode 100644
index 00000000..fc6be602
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-master.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Master
+ * extends the Jetpack_Sitemap_Buffer class to represent the master sitemap
+ * buffer.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing master sitemap xml files.
+ *
+ * @since 5.3.0
+ */
+class Jetpack_Sitemap_Buffer_Master extends Jetpack_Sitemap_Buffer {
+
+ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) {
+ parent::__construct( $item_limit, $byte_limit, $time );
+
+ $this->doc->appendChild(
+ $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" )
+ );
+
+ $this->doc->appendChild(
+ $this->doc->createProcessingInstruction(
+ 'xml-stylesheet',
+ 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'sitemap-index.xsl' ) . '"'
+ )
+ );
+ }
+
+ protected function get_root_element() {
+ if ( ! isset( $this->root ) ) {
+ $this->root = $this->doc->createElement( 'sitemapindex' );
+ $this->root->setAttribute( 'xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9' );
+ $this->doc->appendChild( $this->root );
+ $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-news-fallback.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-news-fallback.php
new file mode 100644
index 00000000..05d4c631
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-news-fallback.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_News
+ * extends the Jetpack_Sitemap_Buffer class to represent the single news sitemap
+ * buffer.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing sitemap image xml files for users without libxml support.
+ *
+ * @since 5.3.0
+ */
+class Jetpack_Sitemap_Buffer_News extends Jetpack_Sitemap_Buffer_Fallback {
+
+ protected function get_root_element() {
+ if ( ! isset( $this->root ) ) {
+
+ /**
+ * Filter the attribute value pairs used for namespace and namespace URI mappings.
+ *
+ * @module sitemaps
+ *
+ * @since 4.8.0
+ *
+ * @param array $namespaces Associative array with namespaces and namespace URIs.
+ */
+ $namespaces = apply_filters(
+ 'jetpack_sitemap_news_ns',
+ array(
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
+ 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
+ 'xmlns:news' => 'http://www.google.com/schemas/sitemap-news/0.9',
+ )
+ );
+
+ $jetpack_version = JETPACK__VERSION;
+ $news_sitemap_xsl_url = $this->finder->construct_sitemap_url( 'news-sitemap.xsl' );
+
+ $this->root = array(
+ "<!-- generator='jetpack-{$jetpack_version}' -->" . PHP_EOL
+ . "<?xml-stylesheet type='text/xsl' href='{$news_sitemap_xsl_url}'?>" . PHP_EOL
+ . '<urlset ' . $this->array_to_xml_attr_string( $namespaces ) . '>',
+ '</urlset>',
+ );
+
+ $this->byte_capacity -= strlen( join( '', $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-news.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-news.php
new file mode 100644
index 00000000..0e5b0327
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-news.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_News
+ * extends the Jetpack_Sitemap_Buffer class to represent the single news sitemap
+ * buffer.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing sitemap image xml files.
+ *
+ * @since 5.3.0
+ */
+class Jetpack_Sitemap_Buffer_News extends Jetpack_Sitemap_Buffer {
+
+ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) {
+ parent::__construct( $item_limit, $byte_limit, $time );
+
+ $this->doc->appendChild(
+ $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" )
+ );
+
+ $this->doc->appendChild(
+ $this->doc->createProcessingInstruction(
+ 'xml-stylesheet',
+ 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'news-sitemap.xsl' ) . '"'
+ )
+ );
+ }
+
+ protected function get_root_element() {
+ if ( ! isset( $this->root ) ) {
+
+ /**
+ * Filter the attribute value pairs used for namespace and namespace URI mappings.
+ *
+ * @module sitemaps
+ *
+ * @since 4.8.0
+ *
+ * @param array $namespaces Associative array with namespaces and namespace URIs.
+ */
+ $namespaces = apply_filters(
+ 'jetpack_sitemap_news_ns',
+ array(
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
+ 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
+ 'xmlns:news' => 'http://www.google.com/schemas/sitemap-news/0.9',
+ )
+ );
+
+ $this->root = $this->doc->createElement( 'urlset' );
+
+ foreach ( $namespaces as $name => $value ) {
+ $this->root->setAttribute( $name, $value );
+ }
+
+ $this->doc->appendChild( $this->root );
+ $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-page-fallback.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-page-fallback.php
new file mode 100644
index 00000000..7cc67af8
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-page-fallback.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Page
+ * extends the Jetpack_Sitemap_Buffer class to represent the single page sitemap
+ * buffer.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing sitemap page xml files for users with no libxml support.
+ *
+ * @since 5.3.0
+ */
+class Jetpack_Sitemap_Buffer_Page extends Jetpack_Sitemap_Buffer_Fallback {
+
+ protected function get_root_element() {
+ if ( ! isset( $this->root ) ) {
+
+ /**
+ * Filter the attribute value pairs used for namespace and namespace URI mappings.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param array $namespaces Associative array with namespaces and namespace URIs.
+ */
+ $namespaces = apply_filters(
+ 'jetpack_sitemap_ns',
+ array(
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
+ 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
+ )
+ );
+
+ $jetpack_version = JETPACK__VERSION;
+ $sitemap_xsl_url = $this->finder->construct_sitemap_url( 'sitemap.xsl' );
+
+ $this->root = array(
+ "<!-- generator='jetpack-{$jetpack_version}' -->" . PHP_EOL
+ . "<?xml-stylesheet type='text/xsl' href='{$sitemap_xsl_url}'?>" . PHP_EOL
+ . '<urlset ' . $this->array_to_xml_attr_string( $namespaces ) . '>',
+ '</urlset>',
+ );
+
+ $this->byte_capacity -= strlen( join( '', $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-page.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-page.php
new file mode 100644
index 00000000..d6885900
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-page.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Page
+ * extends the Jetpack_Sitemap_Buffer class to represent the single page sitemap
+ * buffer.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing sitemap page xml files.
+ *
+ * @since 5.3.0
+ */
+class Jetpack_Sitemap_Buffer_Page extends Jetpack_Sitemap_Buffer {
+
+ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) {
+ parent::__construct( $item_limit, $byte_limit, $time );
+
+ $this->doc->appendChild(
+ $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" )
+ );
+
+ $this->doc->appendChild(
+ $this->doc->createProcessingInstruction(
+ 'xml-stylesheet',
+ 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'sitemap.xsl' ) . '"'
+ )
+ );
+ }
+
+ protected function get_root_element() {
+ if ( ! isset( $this->root ) ) {
+
+ /**
+ * Filter the attribute value pairs used for namespace and namespace URI mappings.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param array $namespaces Associative array with namespaces and namespace URIs.
+ */
+ $namespaces = apply_filters(
+ 'jetpack_sitemap_ns',
+ array(
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
+ 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
+ )
+ );
+
+ $this->root = $this->doc->createElement( 'urlset' );
+
+ foreach ( $namespaces as $name => $value ) {
+ $this->root->setAttribute( $name, $value );
+ }
+
+ $this->doc->appendChild( $this->root );
+ $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-video-fallback.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-video-fallback.php
new file mode 100644
index 00000000..5efe8247
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-video-fallback.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Video
+ * extends the Jetpack_Sitemap_Buffer class to represent the single video sitemap
+ * buffer.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing sitemap video xml files for users without libxml support.
+ *
+ * @since 5.3.0
+ */
+class Jetpack_Sitemap_Buffer_Video extends Jetpack_Sitemap_Buffer_Fallback {
+
+ protected function get_root_element() {
+ if ( ! isset( $this->root ) ) {
+
+ /**
+ * Filter the XML namespaces included in video sitemaps.
+ *
+ * @module sitemaps
+ *
+ * @since 4.8.0
+ *
+ * @param array $namespaces Associative array with namespaces and namespace URIs.
+ */
+ $namespaces = apply_filters(
+ 'jetpack_sitemap_video_ns',
+ array(
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
+ 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
+ 'xmlns:video' => 'http://www.google.com/schemas/sitemap-video/1.1',
+ )
+ );
+
+ $video_sitemap_xsl_url = $this->finder->construct_sitemap_url( 'video-sitemap.xsl' );
+ $jetpack_version = JETPACK__VERSION;
+
+ $this->root = array(
+ "<!-- generator='jetpack-{$jetpack_version}' -->" . PHP_EOL
+ . "<?xml-stylesheet type='text/xsl' href='{$video_sitemap_xsl_url}'?>" . PHP_EOL
+ . '<urlset ' . $this->array_to_xml_attr_string( $namespaces ) . '>',
+ '</urlset>',
+ );
+
+ $this->byte_capacity -= strlen( join( '', $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer-video.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer-video.php
new file mode 100644
index 00000000..c6faff36
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer-video.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Video
+ * extends the Jetpack_Sitemap_Buffer class to represent the single video sitemap
+ * buffer.
+ *
+ * @since 5.3.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing sitemap video xml files.
+ *
+ * @since 5.3.0
+ */
+class Jetpack_Sitemap_Buffer_Video extends Jetpack_Sitemap_Buffer {
+
+ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) {
+ parent::__construct( $item_limit, $byte_limit, $time );
+
+ $this->doc->appendChild(
+ $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" )
+ );
+
+ $this->doc->appendChild(
+ $this->doc->createProcessingInstruction(
+ 'xml-stylesheet',
+ 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'video-sitemap.xsl' ) . '"'
+ )
+ );
+ }
+
+ protected function get_root_element() {
+ if ( ! isset( $this->root ) ) {
+
+ /**
+ * Filter the XML namespaces included in video sitemaps.
+ *
+ * @module sitemaps
+ *
+ * @since 4.8.0
+ *
+ * @param array $namespaces Associative array with namespaces and namespace URIs.
+ */
+ $namespaces = apply_filters(
+ 'jetpack_sitemap_video_ns',
+ array(
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
+ 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
+ 'xmlns:video' => 'http://www.google.com/schemas/sitemap-video/1.1',
+ )
+ );
+
+ $this->root = $this->doc->createElement( 'urlset' );
+
+ foreach ( $namespaces as $name => $value ) {
+ $this->root->setAttribute( $name, $value );
+ }
+
+ $this->doc->appendChild( $this->root );
+ $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-buffer.php b/plugins/jetpack/modules/sitemaps/sitemap-buffer.php
new file mode 100644
index 00000000..98751664
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-buffer.php
@@ -0,0 +1,325 @@
+<?php
+/**
+ * Sitemaps (per the protocol) are essentially lists of XML fragments;
+ * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer
+ * class abstracts the details of constructing these lists while
+ * maintaining the constraints.
+ *
+ * @since 4.8.0
+ * @package Jetpack
+ */
+
+/**
+ * A buffer for constructing sitemap xml files.
+ *
+ * Models a list of strings such that
+ *
+ * 1. the list must have a bounded number of entries,
+ * 2. the concatenation of the strings must have bounded
+ * length (including some header and footer strings), and
+ * 3. each item has a timestamp, and we need to keep track
+ * of the most recent timestamp of the items in the list.
+ *
+ * @since 4.8.0
+ */
+abstract class Jetpack_Sitemap_Buffer {
+
+ /**
+ * Largest number of items the buffer can hold.
+ *
+ * @access protected
+ * @since 4.8.0
+ * @var int $item_capacity The item capacity.
+ */
+ protected $item_capacity;
+
+ /**
+ * Largest number of bytes the buffer can hold.
+ *
+ * @access protected
+ * @since 4.8.0
+ * @var int $byte_capacity The byte capacity.
+ */
+ protected $byte_capacity;
+
+ /**
+ * Flag which detects when the buffer is full.
+ *
+ * @access protected
+ * @since 4.8.0
+ * @var bool $is_full_flag The flag value. This flag is set to false on construction and only flipped to true if we've tried to add something and failed.
+ */
+ protected $is_full_flag;
+
+ /**
+ * Flag which detects when the buffer is empty.
+ *
+ * @access protected
+ * @since 4.8.0
+ * @var bool $is_empty_flag The flag value. This flag is set to true on construction and only flipped to false if we've tried to add something and succeeded.
+ */
+ protected $is_empty_flag;
+
+ /**
+ * The most recent timestamp seen by the buffer.
+ *
+ * @access protected
+ * @since 4.8.0
+ * @var string $timestamp Must be in 'YYYY-MM-DD hh:mm:ss' format.
+ */
+ protected $timestamp;
+
+ /**
+ * The DOM document object that is currently being used to construct the XML doc.
+ *
+ * @access protected
+ * @since 5.3.0
+ * @var DOMDocument $doc
+ */
+ protected $doc = null;
+
+ /**
+ * The root DOM element object that holds everything inside. Do not use directly, call
+ * the get_root_element getter method instead.
+ *
+ * @access protected
+ * @since 5.3.0
+ * @var DOMElement $doc
+ */
+ protected $root = null;
+
+ /**
+ * Helper class to construct sitemap paths.
+ *
+ * @since 5.3.0
+ * @protected
+ * @var Jetpack_Sitemap_Finder
+ */
+ protected $finder;
+
+ /**
+ * Construct a new Jetpack_Sitemap_Buffer.
+ *
+ * @since 4.8.0
+ *
+ * @param int $item_limit The maximum size of the buffer in items.
+ * @param int $byte_limit The maximum size of the buffer in bytes.
+ * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format.
+ */
+ public function __construct( $item_limit, $byte_limit, $time ) {
+ $this->is_full_flag = false;
+ $this->timestamp = $time;
+
+ $this->finder = new Jetpack_Sitemap_Finder();
+ $this->doc = new DOMDocument( '1.0', 'UTF-8' );
+
+ $this->item_capacity = max( 1, intval( $item_limit ) );
+ $this->byte_capacity = max( 1, intval( $byte_limit ) ) - strlen( $this->doc->saveXML() );
+ }
+
+ /**
+ * Returns a DOM element that contains all sitemap elements.
+ *
+ * @access protected
+ * @since 5.3.0
+ * @return DOMElement $root
+ */
+ abstract protected function get_root_element();
+
+ /**
+ * Append an item to the buffer, if there is room for it,
+ * and set is_empty_flag to false. If there is no room,
+ * we set is_full_flag to true. If $item is null,
+ * don't do anything and report success.
+ *
+ * @since 4.8.0
+ * @deprecated 5.3.0 Use Jetpack_Sitemap_Buffer::append.
+ *
+ * @param string $item The item to be added.
+ */
+ public function try_to_add_item( $item ) {
+ _deprecated_function(
+ 'Jetpack_Sitemap_Buffer::try_to_add_item',
+ '5.3.0',
+ 'Jetpack_Sitemap_Buffer::append'
+ );
+ $this->append( $item );
+ }
+
+ /**
+ * Append an item to the buffer, if there is room for it,
+ * and set is_empty_flag to false. If there is no room,
+ * we set is_full_flag to true. If $item is null,
+ * don't do anything and report success.
+ *
+ * @since 5.3.0
+ *
+ * @param array $array The item to be added.
+ *
+ * @return bool True if the append succeeded, False if not.
+ */
+ public function append( $array ) {
+ if ( is_null( $array ) ) {
+ return true;
+ }
+
+ if ( $this->is_full_flag ) {
+ return false;
+ }
+
+ if ( 0 >= $this->item_capacity || 0 >= $this->byte_capacity ) {
+ $this->is_full_flag = true;
+ return false;
+ } else {
+ $this->item_capacity -= 1;
+ $added_element = $this->array_to_xml_string( $array, $this->get_root_element(), $this->doc );
+
+ $this->byte_capacity -= strlen( $this->doc->saveXML( $added_element ) );
+
+ return true;
+ }
+ }
+
+ /**
+ * Retrieve the contents of the buffer.
+ *
+ * @since 4.8.0
+ *
+ * @return string The contents of the buffer (with the footer included).
+ */
+ public function contents() {
+ if ( $this->is_empty() ) {
+ // The sitemap should have at least the root element added to the DOM.
+ $this->get_root_element();
+ }
+ return $this->doc->saveXML();
+ }
+
+ /**
+ * Retrieve the document object.
+ *
+ * @since 5.3.0
+ * @return DOMDocument $doc
+ */
+ public function get_document() {
+ return $this->doc;
+ }
+
+ /**
+ * Detect whether the buffer is full.
+ *
+ * @since 4.8.0
+ *
+ * @return bool True if the buffer is full, false otherwise.
+ */
+ public function is_full() {
+ return $this->is_full_flag;
+ }
+
+ /**
+ * Detect whether the buffer is empty.
+ *
+ * @since 4.8.0
+ *
+ * @return bool True if the buffer is empty, false otherwise.
+ */
+ public function is_empty() {
+ return (
+ ! isset( $this->root )
+ || ! $this->root->hasChildNodes()
+ );
+ }
+
+ /**
+ * Update the timestamp of the buffer.
+ *
+ * @since 4.8.0
+ *
+ * @param string $new_time A datetime string in 'YYYY-MM-DD hh:mm:ss' format.
+ */
+ public function view_time( $new_time ) {
+ $this->timestamp = max( $this->timestamp, $new_time );
+ }
+
+ /**
+ * Retrieve the timestamp of the buffer.
+ *
+ * @since 4.8.0
+ *
+ * @return string A datetime string in 'YYYY-MM-DD hh:mm:ss' format.
+ */
+ public function last_modified() {
+ return $this->timestamp;
+ }
+
+ /**
+ * Render an associative array as an XML string. This is needed because
+ * SimpleXMLElement only handles valid XML, but we sometimes want to
+ * pass around (possibly invalid) fragments. Note that 'null' values make
+ * a tag self-closing; this is only sometimes correct (depending on the
+ * version of HTML/XML); see the list of 'void tags'.
+ *
+ * Example:
+ *
+ * array(
+ * 'html' => array( |<html xmlns="foo">
+ * 'head' => array( | <head>
+ * 'title' => 'Woo!', | <title>Woo!</title>
+ * ), | </head>
+ * 'body' => array( ==> | <body>
+ * 'h2' => 'Some thing', | <h2>Some thing</h2>
+ * 'p' => 'it's all up ons', | <p>it's all up ons</p>
+ * 'br' => null, | <br />
+ * ), | </body>
+ * ), |</html>
+ * )
+ *
+ * @access protected
+ * @since 3.9.0
+ * @since 4.8.0 Rename, add $depth parameter, and change return type.
+ * @since 5.3.0 Refactor, remove $depth parameter, add $parent and $root, make access protected.
+ *
+ * @param array $array A recursive associative array of tag/child relationships.
+ * @param DOMElement $parent (optional) an element to which new children should be added.
+ * @param DOMDocument $root (optional) the parent document.
+ *
+ * @return string|DOMDocument The rendered XML string or an object if root element is specified.
+ */
+ protected function array_to_xml_string( $array, $parent = null, $root = null ) {
+ $return_string = false;
+
+ if ( null === $parent ) {
+ $return_string = true;
+ $parent = $root = new DOMDocument();
+ }
+
+ if ( is_array( $array ) ) {
+
+ foreach ( $array as $key => $value ) {
+ $element = $root->createElement( $key );
+ $parent->appendChild( $element );
+
+ if ( is_array( $value ) ) {
+ foreach ( $value as $child_key => $child_value ) {
+ $child = $root->createElement( $child_key );
+ $element->appendChild( $child );
+ $child->appendChild( self::array_to_xml_string( $child_value, $child, $root ) );
+ }
+ } else {
+ $element->appendChild(
+ $root->createTextNode( $value )
+ );
+ }
+ }
+ } else {
+ $element = $root->createTextNode( $array );
+ $parent->appendChild( $element );
+ }
+
+ if ( $return_string ) {
+ return $root->saveHTML();
+ } else {
+ return $element;
+ }
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-builder.php b/plugins/jetpack/modules/sitemaps/sitemap-builder.php
new file mode 100644
index 00000000..e04f58d5
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-builder.php
@@ -0,0 +1,1468 @@
+<?php
+/**
+ * Build the sitemap tree.
+ *
+ * @package Jetpack
+ * @since 4.8.0
+ * @author Automattic
+ */
+
+/* Include sitemap subclasses, if not already, and include proper buffer based on phpxml's availability. */
+require_once dirname( __FILE__ ) . '/sitemap-constants.php';
+require_once dirname( __FILE__ ) . '/sitemap-buffer.php';
+
+if ( ! class_exists( 'DOMDocument' ) ) {
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-fallback.php';
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-image-fallback.php';
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-master-fallback.php';
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-news-fallback.php';
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-page-fallback.php';
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-video-fallback.php';
+} else {
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-image.php';
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-master.php';
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-news.php';
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-page.php';
+ require_once dirname( __FILE__ ) . '/sitemap-buffer-video.php';
+}
+
+require_once dirname( __FILE__ ) . '/sitemap-librarian.php';
+require_once dirname( __FILE__ ) . '/sitemap-finder.php';
+require_once dirname( __FILE__ ) . '/sitemap-state.php';
+
+if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+ require_once dirname( __FILE__ ) . '/sitemap-logger.php';
+}
+
+/**
+ * Simple class for rendering an empty sitemap with a short TTL
+ */
+class Jetpack_Sitemap_Buffer_Empty extends Jetpack_Sitemap_Buffer {
+
+ public function __construct() {
+ parent::__construct( JP_SITEMAP_MAX_ITEMS, JP_SITEMAP_MAX_BYTES, '1970-01-01 00:00:00' );
+
+ $this->doc->appendChild(
+ $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" )
+ );
+
+ $this->doc->appendChild(
+ $this->doc->createProcessingInstruction(
+ 'xml-stylesheet',
+ 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'sitemap-index.xsl' ) . '"'
+ )
+ );
+ }
+
+ protected function get_root_element() {
+ if ( ! isset( $this->root ) ) {
+ $this->root = $this->doc->createElement( 'sitemapindex' );
+ $this->root->setAttribute( 'xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9' );
+ $this->doc->appendChild( $this->root );
+ $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) );
+ }
+
+ return $this->root;
+ }
+}
+
+/**
+ * The Jetpack_Sitemap_Builder object handles the construction of
+ * all sitemap files (except the XSL files, which are handled by
+ * Jetpack_Sitemap_Stylist.) Other than the constructor, there are
+ * only two public functions: build_all_sitemaps and news_sitemap_xml.
+ *
+ * @since 4.8.0
+ */
+class Jetpack_Sitemap_Builder {
+
+ /**
+ * Librarian object for storing and retrieving sitemap data.
+ *
+ * @access private
+ * @since 4.8.0
+ * @var $librarian Jetpack_Sitemap_Librarian
+ */
+ private $librarian;
+
+ /**
+ * Logger object for reporting debug messages.
+ *
+ * @access private
+ * @since 4.8.0
+ * @var $logger Jetpack_Sitemap_Logger
+ */
+ private $logger = false;
+
+ /**
+ * Finder object for dealing with sitemap URIs.
+ *
+ * @access private
+ * @since 4.8.0
+ * @var $finder Jetpack_Sitemap_Finder
+ */
+ private $finder;
+
+ /**
+ * Construct a new Jetpack_Sitemap_Builder object.
+ *
+ * @access public
+ * @since 4.8.0
+ */
+ public function __construct() {
+ $this->librarian = new Jetpack_Sitemap_Librarian();
+ $this->finder = new Jetpack_Sitemap_Finder();
+
+ if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+ $this->logger = new Jetpack_Sitemap_Logger();
+ }
+
+ update_option(
+ 'jetpack_sitemap_post_types',
+ /**
+ * The array of post types to be included in the sitemap.
+ *
+ * Add your custom post type name to the array to have posts of
+ * that type included in the sitemap. The default array includes
+ * 'page' and 'post'.
+ *
+ * The result of this filter is cached in an option, 'jetpack_sitemap_post_types',
+ * so this filter only has to be applied once per generation.
+ *
+ * @since 4.8.0
+ */
+ apply_filters(
+ 'jetpack_sitemap_post_types',
+ array( 'post', 'page' )
+ )
+ );
+ }
+
+ /**
+ * Update the sitemap.
+ *
+ * All we do here is call build_next_sitemap_file a bunch of times.
+ *
+ * @since 4.8.0
+ */
+ public function update_sitemap() {
+ if ( $this->logger ) {
+ $this->logger->report( '-- Updating...' );
+ if ( ! class_exists( 'DOMDocument' ) ) {
+ $this->logger->report(
+ __(
+ 'Jetpack can not load necessary XML manipulation libraries. Please ask your hosting provider to refer to our server requirements at https://jetpack.com/support/server-requirements/ .',
+ 'jetpack'
+ ),
+ true
+ );
+ }
+ }
+
+ for ( $i = 1; $i <= JP_SITEMAP_UPDATE_SIZE; $i++ ) {
+ if ( true === $this->build_next_sitemap_file() ) {
+ break; // All finished!
+ }
+ }
+
+ if ( $this->logger ) {
+ $this->logger->report( '-- ...done for now.' );
+ $this->logger->time();
+ }
+ }
+
+ /**
+ * Generate the next sitemap file.
+ *
+ * Reads the most recent state of the sitemap generation phase,
+ * constructs the next file, and updates the state.
+ *
+ * @since 4.8.0
+ *
+ * @return bool True when finished.
+ */
+ private function build_next_sitemap_file() {
+ $finished = false; // Initialize finished flag.
+
+ // Get the most recent state, and lock the state.
+ $state = Jetpack_Sitemap_State::check_out();
+
+ // Do nothing if the state was locked.
+ if ( false === $state ) {
+ return false;
+ }
+
+ // Otherwise, branch on the sitemap-type key of $state.
+ switch ( $state['sitemap-type'] ) {
+ case JP_PAGE_SITEMAP_TYPE:
+ $this->build_next_sitemap_of_type(
+ JP_PAGE_SITEMAP_TYPE,
+ array( $this, 'build_one_page_sitemap' ),
+ $state
+ );
+ break;
+
+ case JP_PAGE_SITEMAP_INDEX_TYPE:
+ $this->build_next_sitemap_index_of_type(
+ JP_PAGE_SITEMAP_INDEX_TYPE,
+ JP_IMAGE_SITEMAP_TYPE,
+ $state
+ );
+ break;
+
+ case JP_IMAGE_SITEMAP_TYPE:
+ $this->build_next_sitemap_of_type(
+ JP_IMAGE_SITEMAP_TYPE,
+ array( $this, 'build_one_image_sitemap' ),
+ $state
+ );
+ break;
+
+ case JP_IMAGE_SITEMAP_INDEX_TYPE:
+ $this->build_next_sitemap_index_of_type(
+ JP_IMAGE_SITEMAP_INDEX_TYPE,
+ JP_VIDEO_SITEMAP_TYPE,
+ $state
+ );
+ break;
+
+ case JP_VIDEO_SITEMAP_TYPE:
+ $this->build_next_sitemap_of_type(
+ JP_VIDEO_SITEMAP_TYPE,
+ array( $this, 'build_one_video_sitemap' ),
+ $state
+ );
+ break;
+
+ case JP_VIDEO_SITEMAP_INDEX_TYPE:
+ $this->build_next_sitemap_index_of_type(
+ JP_VIDEO_SITEMAP_INDEX_TYPE,
+ JP_MASTER_SITEMAP_TYPE,
+ $state
+ );
+ break;
+
+ case JP_MASTER_SITEMAP_TYPE:
+ $this->build_master_sitemap( $state['max'] );
+
+ // Reset the state and quit.
+ Jetpack_Sitemap_State::reset(
+ JP_PAGE_SITEMAP_TYPE
+ );
+
+ if ( $this->logger ) {
+ $this->logger->report( '-- Finished.' );
+ $this->logger->time();
+ }
+ $finished = true;
+
+ break;
+
+ default:
+ Jetpack_Sitemap_State::reset(
+ JP_PAGE_SITEMAP_TYPE
+ );
+ $finished = true;
+
+ break;
+ } // End switch.
+
+ // Unlock the state.
+ Jetpack_Sitemap_State::unlock();
+
+ return $finished;
+ }
+
+ /**
+ * Build the next sitemap of a given type and update the sitemap state.
+ *
+ * @since 4.8.0
+ *
+ * @param string $sitemap_type The type of the sitemap being generated.
+ * @param callback $build_one A callback which builds a single sitemap file.
+ * @param array $state A sitemap state.
+ */
+ private function build_next_sitemap_of_type( $sitemap_type, $build_one, $state ) {
+ $index_type = jp_sitemap_index_type_of( $sitemap_type );
+
+ // Try to build a sitemap.
+ $result = call_user_func_array(
+ $build_one,
+ array(
+ $state['number'] + 1,
+ $state['last-added'],
+ )
+ );
+
+ if ( false === $result ) {
+ // If no sitemap was generated, advance to the next type.
+ Jetpack_Sitemap_State::check_in(
+ array(
+ 'sitemap-type' => $index_type,
+ 'last-added' => 0,
+ 'number' => 0,
+ 'last-modified' => '1970-01-01 00:00:00',
+ )
+ );
+
+ if ( $this->logger ) {
+ $this->logger->report( "-- Cleaning Up $sitemap_type" );
+ }
+
+ // Clean up old files.
+ $this->librarian->delete_numbered_sitemap_rows_after(
+ $state['number'],
+ $sitemap_type
+ );
+
+ return;
+ }
+
+ // Otherwise, update the state.
+ Jetpack_Sitemap_State::check_in(
+ array(
+ 'sitemap-type' => $state['sitemap-type'],
+ 'last-added' => $result['last_id'],
+ 'number' => $state['number'] + 1,
+ 'last-modified' => $result['last_modified'],
+ )
+ );
+
+ if ( true === $result['any_left'] ) {
+ // If there's more work to be done with this type, return.
+ return;
+ }
+
+ // Otherwise, advance state to the next sitemap type.
+ Jetpack_Sitemap_State::check_in(
+ array(
+ 'sitemap-type' => $index_type,
+ 'last-added' => 0,
+ 'number' => 0,
+ 'last-modified' => '1970-01-01 00:00:00',
+ )
+ );
+
+ if ( $this->logger ) {
+ $this->logger->report( "-- Cleaning Up $sitemap_type" );
+ }
+
+ // Clean up old files.
+ $this->librarian->delete_numbered_sitemap_rows_after(
+ $state['number'] + 1,
+ $sitemap_type
+ );
+ }
+
+ /**
+ * Build the next sitemap index of a given type and update the state.
+ *
+ * @since 4.8.0
+ *
+ * @param string $index_type The type of index being generated.
+ * @param string $next_type The next type to generate after this one.
+ * @param array $state A sitemap state.
+ */
+ private function build_next_sitemap_index_of_type( $index_type, $next_type, $state ) {
+ $sitemap_type = jp_sitemap_child_type_of( $index_type );
+
+ // If only 0 or 1 sitemaps were built, advance to the next type and return.
+ if ( 1 >= $state['max'][ $sitemap_type ]['number'] ) {
+ Jetpack_Sitemap_State::check_in(
+ array(
+ 'sitemap-type' => $next_type,
+ 'last-added' => 0,
+ 'number' => 0,
+ 'last-modified' => '1970-01-01 00:00:00',
+ )
+ );
+
+ if ( $this->logger ) {
+ $this->logger->report( "-- Cleaning Up $index_type" );
+ }
+
+ // There are no indices of this type.
+ $this->librarian->delete_numbered_sitemap_rows_after(
+ 0,
+ $index_type
+ );
+
+ return;
+ }
+
+ // Otherwise, try to build a sitemap index.
+ $result = $this->build_one_sitemap_index(
+ $state['number'] + 1,
+ $state['last-added'],
+ $state['last-modified'],
+ $index_type
+ );
+
+ // If no index was built, advance to the next type and return.
+ if ( false === $result ) {
+ Jetpack_Sitemap_State::check_in(
+ array(
+ 'sitemap-type' => $next_type,
+ 'last-added' => 0,
+ 'number' => 0,
+ 'last-modified' => '1970-01-01 00:00:00',
+ )
+ );
+
+ if ( $this->logger ) {
+ $this->logger->report( "-- Cleaning Up $index_type" );
+ }
+
+ // Clean up old files.
+ $this->librarian->delete_numbered_sitemap_rows_after(
+ $state['number'],
+ $index_type
+ );
+
+ return;
+ }
+
+ // Otherwise, check in the state.
+ Jetpack_Sitemap_State::check_in(
+ array(
+ 'sitemap-type' => $index_type,
+ 'last-added' => $result['last_id'],
+ 'number' => $state['number'] + 1,
+ 'last-modified' => $result['last_modified'],
+ )
+ );
+
+ // If there are still sitemaps left to index, return.
+ if ( true === $result['any_left'] ) {
+ return;
+ }
+
+ // Otherwise, advance to the next type.
+ Jetpack_Sitemap_State::check_in(
+ array(
+ 'sitemap-type' => $next_type,
+ 'last-added' => 0,
+ 'number' => 0,
+ 'last-modified' => '1970-01-01 00:00:00',
+ )
+ );
+
+ if ( $this->logger ) {
+ $this->logger->report( "-- Cleaning Up $index_type" );
+ }
+
+ // We're done generating indices of this type.
+ $this->librarian->delete_numbered_sitemap_rows_after(
+ $state['number'] + 1,
+ $index_type
+ );
+ }
+
+ /**
+ * Builds the master sitemap index.
+ *
+ * @param array $max Array of sitemap types with max index and datetime.
+ *
+ * @since 4.8.0
+ */
+ private function build_master_sitemap( $max ) {
+ $page = array();
+ $image = array();
+ $video = array();
+ if ( $this->logger ) {
+ $this->logger->report( '-- Building Master Sitemap.' );
+ }
+
+ $buffer = new Jetpack_Sitemap_Buffer_Master(
+ JP_SITEMAP_MAX_ITEMS,
+ JP_SITEMAP_MAX_BYTES
+ );
+
+ if ( 0 < $max[ JP_PAGE_SITEMAP_TYPE ]['number'] ) {
+ if ( 1 === $max[ JP_PAGE_SITEMAP_TYPE ]['number'] ) {
+ $page['filename'] = jp_sitemap_filename( JP_PAGE_SITEMAP_TYPE, 1 );
+ $page['last_modified'] = jp_sitemap_datetime( $max[ JP_PAGE_SITEMAP_TYPE ]['lastmod'] );
+ } else {
+ $page['filename'] = jp_sitemap_filename(
+ JP_PAGE_SITEMAP_INDEX_TYPE,
+ $max[ JP_PAGE_SITEMAP_INDEX_TYPE ]['number']
+ );
+ $page['last_modified'] = jp_sitemap_datetime( $max[ JP_PAGE_SITEMAP_INDEX_TYPE ]['lastmod'] );
+ }
+
+ $buffer->append(
+ array(
+ 'sitemap' => array(
+ 'loc' => $this->finder->construct_sitemap_url( $page['filename'] ),
+ 'lastmod' => $page['last_modified'],
+ ),
+ )
+ );
+ }
+
+ if ( 0 < $max[ JP_IMAGE_SITEMAP_TYPE ]['number'] ) {
+ if ( 1 === $max[ JP_IMAGE_SITEMAP_TYPE ]['number'] ) {
+ $image['filename'] = jp_sitemap_filename( JP_IMAGE_SITEMAP_TYPE, 1 );
+ $image['last_modified'] = jp_sitemap_datetime( $max[ JP_IMAGE_SITEMAP_TYPE ]['lastmod'] );
+ } else {
+ $image['filename'] = jp_sitemap_filename(
+ JP_IMAGE_SITEMAP_INDEX_TYPE,
+ $max[ JP_IMAGE_SITEMAP_INDEX_TYPE ]['number']
+ );
+ $image['last_modified'] = jp_sitemap_datetime( $max[ JP_IMAGE_SITEMAP_INDEX_TYPE ]['lastmod'] );
+ }
+
+ $buffer->append(
+ array(
+ 'sitemap' => array(
+ 'loc' => $this->finder->construct_sitemap_url( $image['filename'] ),
+ 'lastmod' => $image['last_modified'],
+ ),
+ )
+ );
+ }
+
+ if ( 0 < $max[ JP_VIDEO_SITEMAP_TYPE ]['number'] ) {
+ if ( 1 === $max[ JP_VIDEO_SITEMAP_TYPE ]['number'] ) {
+ $video['filename'] = jp_sitemap_filename( JP_VIDEO_SITEMAP_TYPE, 1 );
+ $video['last_modified'] = jp_sitemap_datetime( $max[ JP_VIDEO_SITEMAP_TYPE ]['lastmod'] );
+ } else {
+ $video['filename'] = jp_sitemap_filename(
+ JP_VIDEO_SITEMAP_INDEX_TYPE,
+ $max[ JP_VIDEO_SITEMAP_INDEX_TYPE ]['number']
+ );
+ $video['last_modified'] = jp_sitemap_datetime( $max[ JP_VIDEO_SITEMAP_INDEX_TYPE ]['lastmod'] );
+ }
+
+ $buffer->append(
+ array(
+ 'sitemap' => array(
+ 'loc' => $this->finder->construct_sitemap_url( $video['filename'] ),
+ 'lastmod' => $video['last_modified'],
+ ),
+ )
+ );
+ }
+
+ $this->librarian->store_sitemap_data(
+ 0,
+ JP_MASTER_SITEMAP_TYPE,
+ $buffer->contents(),
+ ''
+ );
+ }
+
+ /**
+ * Build and store a single page sitemap. Returns false if no sitemap is built.
+ *
+ * Side effect: Create/update a sitemap row.
+ *
+ * @access private
+ * @since 4.8.0
+ *
+ * @param int $number The number of the current sitemap.
+ * @param int $from_id The greatest lower bound of the IDs of the posts to be included.
+ *
+ * @return bool|array @args {
+ * @type int $last_id The ID of the last item to be successfully added to the buffer.
+ * @type bool $any_left 'true' if there are items which haven't been saved to a sitemap, 'false' otherwise.
+ * @type string $last_modified The most recent timestamp to appear on the sitemap.
+ * }
+ */
+ public function build_one_page_sitemap( $number, $from_id ) {
+ $last_post_id = $from_id;
+ $any_posts_left = true;
+
+ if ( $this->logger ) {
+ $debug_name = jp_sitemap_filename( JP_PAGE_SITEMAP_TYPE, $number );
+ $this->logger->report( "-- Building $debug_name" );
+ }
+
+ $buffer = new Jetpack_Sitemap_Buffer_Page(
+ JP_SITEMAP_MAX_ITEMS,
+ JP_SITEMAP_MAX_BYTES
+ );
+
+ // Add entry for the main page (only if we're at the first one) and it isn't already going to be included as a page.
+ if ( 1 === $number && 'page' !== get_option( 'show_on_front' ) ) {
+ $item_array = array(
+ 'url' => array(
+ 'loc' => home_url(),
+ ),
+ );
+
+ /**
+ * Filter associative array with data to build <url> node
+ * and its descendants for site home.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param array $blog_home Data to build parent and children nodes for site home.
+ */
+ $item_array = apply_filters( 'jetpack_sitemap_url_home', $item_array );
+
+ $buffer->append( $item_array );
+ }
+
+ // Add as many items to the buffer as possible.
+ while ( $last_post_id >= 0 && false === $buffer->is_full() ) {
+ $posts = $this->librarian->query_posts_after_id(
+ $last_post_id,
+ JP_SITEMAP_BATCH_SIZE
+ );
+
+ if ( null == $posts ) { // WPCS: loose comparison ok.
+ $any_posts_left = false;
+ break;
+ }
+
+ foreach ( $posts as $post ) {
+ $current_item = $this->post_to_sitemap_item( $post );
+
+ if ( true === $buffer->append( $current_item['xml'] ) ) {
+ $last_post_id = $post->ID;
+ $buffer->view_time( $current_item['last_modified'] );
+ } else {
+ break;
+ }
+ }
+ }
+
+ // Handle other page sitemap URLs.
+ if ( false === $any_posts_left || $last_post_id < 0 ) {
+ // Negative IDs are used to track URL indexes.
+ $last_post_id = min( 0, $last_post_id );
+ $any_posts_left = true; // Reinitialize.
+
+ /**
+ * Filter other page sitemap URLs.
+ *
+ * @module sitemaps
+ *
+ * @since 6.1.0
+ *
+ * @param array $urls An array of other URLs.
+ */
+ $other_urls = apply_filters( 'jetpack_page_sitemap_other_urls', array() );
+
+ if ( $other_urls ) { // Start with index [1].
+ $other_urls = array_values( $other_urls );
+ array_unshift( $other_urls, $other_urls[0] );
+ unset( $other_urls[0] );
+ }
+
+ // Add as many items to the buffer as possible.
+ while ( false === $buffer->is_full() ) {
+ $last_post_id_index = abs( $last_post_id );
+ $start_from_post_id_index = $last_post_id_index ? $last_post_id_index + 1 : 0;
+ $urls = array_slice(
+ $other_urls,
+ $start_from_post_id_index,
+ JP_SITEMAP_BATCH_SIZE,
+ true
+ );
+
+ if ( ! $urls ) {
+ $any_posts_left = false;
+ break;
+ }
+
+ foreach ( $urls as $index => $url ) {
+ if ( ! is_array( $url ) ) {
+ $url = array( 'loc' => $url );
+ }
+ $item = array( 'xml' => compact( 'url' ) );
+
+ if ( true === $buffer->append( $item['xml'] ) ) {
+ $last_post_id = -$index;
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ // If no items were added, return false.
+ if ( true === $buffer->is_empty() ) {
+ return false;
+ }
+
+ /**
+ * Filter sitemap before rendering it as XML.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ * @since 5.3.0 returns an element of DOMDocument type instead of SimpleXMLElement
+ *
+ * @param DOMDocument $doc Data tree for sitemap.
+ * @param string $last_modified Date of last modification.
+ */
+ $tree = apply_filters(
+ 'jetpack_print_sitemap',
+ $buffer->get_document(),
+ $buffer->last_modified()
+ );
+
+ // Store the buffer as the content of a sitemap row.
+ $this->librarian->store_sitemap_data(
+ $number,
+ JP_PAGE_SITEMAP_TYPE,
+ $buffer->contents(),
+ $buffer->last_modified()
+ );
+
+ /*
+ * Now report back with the ID of the last post ID to be
+ * successfully added and whether there are any posts left.
+ */
+ return array(
+ 'last_id' => $last_post_id,
+ 'any_left' => $any_posts_left,
+ 'last_modified' => $buffer->last_modified(),
+ );
+ }
+
+ /**
+ * Build and store a single image sitemap. Returns false if no sitemap is built.
+ *
+ * Side effect: Create/update an image sitemap row.
+ *
+ * @access private
+ * @since 4.8.0
+ *
+ * @param int $number The number of the current sitemap.
+ * @param int $from_id The greatest lower bound of the IDs of the posts to be included.
+ *
+ * @return bool|array @args {
+ * @type int $last_id The ID of the last item to be successfully added to the buffer.
+ * @type bool $any_left 'true' if there are items which haven't been saved to a sitemap, 'false' otherwise.
+ * @type string $last_modified The most recent timestamp to appear on the sitemap.
+ * }
+ */
+ public function build_one_image_sitemap( $number, $from_id ) {
+ $last_post_id = $from_id;
+ $any_posts_left = true;
+
+ if ( $this->logger ) {
+ $debug_name = jp_sitemap_filename( JP_IMAGE_SITEMAP_TYPE, $number );
+ $this->logger->report( "-- Building $debug_name" );
+ }
+
+ $buffer = new Jetpack_Sitemap_Buffer_Image(
+ JP_SITEMAP_MAX_ITEMS,
+ JP_SITEMAP_MAX_BYTES
+ );
+
+ // Add as many items to the buffer as possible.
+ while ( false === $buffer->is_full() ) {
+ $posts = $this->librarian->query_images_after_id(
+ $last_post_id,
+ JP_SITEMAP_BATCH_SIZE
+ );
+
+ if ( null == $posts ) { // WPCS: loose comparison ok.
+ $any_posts_left = false;
+ break;
+ }
+
+ foreach ( $posts as $post ) {
+ $current_item = $this->image_post_to_sitemap_item( $post );
+
+ if ( true === $buffer->append( $current_item['xml'] ) ) {
+ $last_post_id = $post->ID;
+ $buffer->view_time( $current_item['last_modified'] );
+ } else {
+ break;
+ }
+ }
+ }
+
+ // If no items were added, return false.
+ if ( true === $buffer->is_empty() ) {
+ return false;
+ }
+
+ // Store the buffer as the content of a jp_sitemap post.
+ $this->librarian->store_sitemap_data(
+ $number,
+ JP_IMAGE_SITEMAP_TYPE,
+ $buffer->contents(),
+ $buffer->last_modified()
+ );
+
+ /*
+ * Now report back with the ID of the last post to be
+ * successfully added and whether there are any posts left.
+ */
+ return array(
+ 'last_id' => $last_post_id,
+ 'any_left' => $any_posts_left,
+ 'last_modified' => $buffer->last_modified(),
+ );
+ }
+
+ /**
+ * Build and store a single video sitemap. Returns false if no sitemap is built.
+ *
+ * Side effect: Create/update an video sitemap row.
+ *
+ * @access private
+ * @since 4.8.0
+ *
+ * @param int $number The number of the current sitemap.
+ * @param int $from_id The greatest lower bound of the IDs of the posts to be included.
+ *
+ * @return bool|array @args {
+ * @type int $last_id The ID of the last item to be successfully added to the buffer.
+ * @type bool $any_left 'true' if there are items which haven't been saved to a sitemap, 'false' otherwise.
+ * @type string $last_modified The most recent timestamp to appear on the sitemap.
+ * }
+ */
+ public function build_one_video_sitemap( $number, $from_id ) {
+ $last_post_id = $from_id;
+ $any_posts_left = true;
+
+ if ( $this->logger ) {
+ $debug_name = jp_sitemap_filename( JP_VIDEO_SITEMAP_TYPE, $number );
+ $this->logger->report( "-- Building $debug_name" );
+ }
+
+ $buffer = new Jetpack_Sitemap_Buffer_Video(
+ JP_SITEMAP_MAX_ITEMS,
+ JP_SITEMAP_MAX_BYTES
+ );
+
+ // Add as many items to the buffer as possible.
+ while ( false === $buffer->is_full() ) {
+ $posts = $this->librarian->query_videos_after_id(
+ $last_post_id,
+ JP_SITEMAP_BATCH_SIZE
+ );
+
+ if ( null == $posts ) { // WPCS: loose comparison ok.
+ $any_posts_left = false;
+ break;
+ }
+
+ foreach ( $posts as $post ) {
+ $current_item = $this->video_post_to_sitemap_item( $post );
+
+ if ( true === $buffer->append( $current_item['xml'] ) ) {
+ $last_post_id = $post->ID;
+ $buffer->view_time( $current_item['last_modified'] );
+ } else {
+ break;
+ }
+ }
+ }
+
+ // If no items were added, return false.
+ if ( true === $buffer->is_empty() ) {
+ return false;
+ }
+
+ if ( false === $buffer->is_empty() ) {
+ $this->librarian->store_sitemap_data(
+ $number,
+ JP_VIDEO_SITEMAP_TYPE,
+ $buffer->contents(),
+ $buffer->last_modified()
+ );
+ }
+
+ /*
+ * Now report back with the ID of the last post to be
+ * successfully added and whether there are any posts left.
+ */
+ return array(
+ 'last_id' => $last_post_id,
+ 'any_left' => $any_posts_left,
+ 'last_modified' => $buffer->last_modified(),
+ );
+ }
+
+ /**
+ * Build and store a single page sitemap index. Return false if no index is built.
+ *
+ * Side effect: Create/update a sitemap index row.
+ *
+ * @access private
+ * @since 4.8.0
+ *
+ * @param int $number The number of the current sitemap index.
+ * @param int $from_id The greatest lower bound of the IDs of the sitemaps to be included.
+ * @param string $datetime Datetime of previous sitemap in 'YYYY-MM-DD hh:mm:ss' format.
+ * @param string $index_type Sitemap index type.
+ *
+ * @return bool|array @args {
+ * @type int $last_id The ID of the last item to be successfully added to the buffer.
+ * @type bool $any_left 'true' if there are items which haven't been saved to a sitemap, 'false' otherwise.
+ * @type string $last_modified The most recent timestamp to appear on the sitemap.
+ * }
+ */
+ private function build_one_sitemap_index( $number, $from_id, $datetime, $index_type ) {
+ $last_sitemap_id = $from_id;
+ $any_sitemaps_left = true;
+
+ // Check the datetime format.
+ $datetime = jp_sitemap_datetime( $datetime );
+
+ $sitemap_type = jp_sitemap_child_type_of( $index_type );
+
+ if ( $this->logger ) {
+ $index_debug_name = jp_sitemap_filename( $index_type, $number );
+ $this->logger->report( "-- Building $index_debug_name" );
+ }
+
+ $buffer = new Jetpack_Sitemap_Buffer_Master(
+ JP_SITEMAP_MAX_ITEMS,
+ JP_SITEMAP_MAX_BYTES,
+ $datetime
+ );
+
+ // Add pointer to the previous sitemap index (unless we're at the first one).
+ if ( 1 !== $number ) {
+ $i = $number - 1;
+ $prev_index_url = $this->finder->construct_sitemap_url(
+ jp_sitemap_filename( $index_type, $i )
+ );
+
+ $item_array = array(
+ 'sitemap' => array(
+ 'loc' => $prev_index_url,
+ 'lastmod' => $datetime,
+ ),
+ );
+
+ $buffer->append( $item_array );
+ }
+
+ // Add as many items to the buffer as possible.
+ while ( false === $buffer->is_full() ) {
+ // Retrieve a batch of posts (in order).
+ $posts = $this->librarian->query_sitemaps_after_id(
+ $sitemap_type,
+ $last_sitemap_id,
+ JP_SITEMAP_BATCH_SIZE
+ );
+
+ // If there were no posts to get, make a note.
+ if ( null == $posts ) { // WPCS: loose comparison ok.
+ $any_sitemaps_left = false;
+ break;
+ }
+
+ // Otherwise, loop through each post in the batch.
+ foreach ( $posts as $post ) {
+ // Generate the sitemap XML for the post.
+ $current_item = $this->sitemap_row_to_index_item( (array) $post );
+
+ // Try adding this item to the buffer.
+ if ( true === $buffer->append( $current_item['xml'] ) ) {
+ $last_sitemap_id = $post['ID'];
+ $buffer->view_time( $current_item['last_modified'] );
+ } else {
+ // Otherwise stop looping through posts.
+ break;
+ }
+ }
+ }
+
+ // If no items were added, return false.
+ if ( true === $buffer->is_empty() ) {
+ return false;
+ }
+
+ $this->librarian->store_sitemap_data(
+ $number,
+ $index_type,
+ $buffer->contents(),
+ $buffer->last_modified()
+ );
+
+ /*
+ * Now report back with the ID of the last sitemap post ID to
+ * be successfully added, whether there are any sitemap posts
+ * left, and the most recent modification time seen.
+ */
+ return array(
+ 'last_id' => $last_sitemap_id,
+ 'any_left' => $any_sitemaps_left,
+ 'last_modified' => $buffer->last_modified(),
+ );
+ }
+
+ /**
+ * Construct the sitemap index url entry for a sitemap row.
+ *
+ * @link http://www.sitemaps.org/protocol.html#sitemapIndex_sitemap
+ *
+ * @access private
+ * @since 4.8.0
+ *
+ * @param array $row The sitemap data to be processed.
+ *
+ * @return string An XML fragment representing the post URL.
+ */
+ private function sitemap_row_to_index_item( $row ) {
+ $url = $this->finder->construct_sitemap_url( $row['post_title'] );
+
+ $item_array = array(
+ 'sitemap' => array(
+ 'loc' => $url,
+ 'lastmod' => jp_sitemap_datetime( $row['post_date'] ),
+ ),
+ );
+
+ return array(
+ 'xml' => $item_array,
+ 'last_modified' => $row['post_date'],
+ );
+ }
+
+
+ /**
+ * This is served instead of a 404 when the master sitemap is requested
+ * but not yet generated.
+ *
+ * @access public
+ * @since 6.7.0
+ *
+ * @return string The empty sitemap xml.
+ */
+ public function empty_sitemap_xml() {
+ $empty_sitemap = new Jetpack_Sitemap_Buffer_Empty();
+ return $empty_sitemap->contents();
+ }
+
+ /**
+ * Build and return the news sitemap xml. Note that the result of this
+ * function is cached in the transient 'jetpack_news_sitemap_xml'.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @return string The news sitemap xml.
+ */
+ public function news_sitemap_xml() {
+ $the_stored_news_sitemap = get_transient( 'jetpack_news_sitemap_xml' );
+
+ if ( false === $the_stored_news_sitemap ) {
+
+ if ( $this->logger ) {
+ $this->logger->report( 'Beginning news sitemap generation.' );
+ }
+
+ /**
+ * Filter limit of entries to include in news sitemap.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param int $count Number of entries to include in news sitemap.
+ */
+ $item_limit = apply_filters(
+ 'jetpack_sitemap_news_sitemap_count',
+ JP_NEWS_SITEMAP_MAX_ITEMS
+ );
+
+ $buffer = new Jetpack_Sitemap_Buffer_News(
+ min( $item_limit, JP_NEWS_SITEMAP_MAX_ITEMS ),
+ JP_SITEMAP_MAX_BYTES
+ );
+
+ $posts = $this->librarian->query_most_recent_posts( JP_NEWS_SITEMAP_MAX_ITEMS );
+
+ foreach ( $posts as $post ) {
+ $current_item = $this->post_to_news_sitemap_item( $post );
+
+ if ( false === $buffer->append( $current_item['xml'] ) ) {
+ break;
+ }
+ }
+
+ if ( $this->logger ) {
+ $this->logger->time( 'End news sitemap generation.' );
+ }
+
+ $the_stored_news_sitemap = $buffer->contents();
+
+ set_transient(
+ 'jetpack_news_sitemap_xml',
+ $the_stored_news_sitemap,
+ JP_NEWS_SITEMAP_INTERVAL
+ );
+ } // End if.
+
+ return $the_stored_news_sitemap;
+ }
+
+ /**
+ * Construct the sitemap url entry for a WP_Post.
+ *
+ * @link http://www.sitemaps.org/protocol.html#urldef
+ * @access private
+ * @since 4.8.0
+ *
+ * @param WP_Post $post The post to be processed.
+ *
+ * @return array
+ * @type array $xml An XML fragment representing the post URL.
+ * @type string $last_modified Date post was last modified.
+ */
+ private function post_to_sitemap_item( $post ) {
+
+ /**
+ * Filter condition to allow skipping specific posts in sitemap.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param bool $skip Current boolean. False by default, so no post is skipped.
+ * @param object $post Current post in the form of a $wpdb result object. Not WP_Post.
+ */
+ if ( true === apply_filters( 'jetpack_sitemap_skip_post', false, $post ) ) {
+ return array(
+ 'xml' => null,
+ 'last_modified' => null,
+ );
+ }
+
+ $url = esc_url( get_permalink( $post ) );
+
+ /*
+ * Spec requires the URL to be <=2048 bytes.
+ * In practice this constraint is unlikely to be violated.
+ */
+ if ( 2048 < strlen( $url ) ) {
+ $url = home_url() . '/?p=' . $post->ID;
+ }
+
+ $last_modified = $post->post_modified_gmt;
+
+ // Check for more recent comments.
+ // Note that 'Y-m-d h:i:s' strings sort lexicographically.
+ if ( 0 < $post->comment_count ) {
+ $last_modified = max(
+ $last_modified,
+ $this->librarian->query_latest_approved_comment_time_on_post( $post->ID )
+ );
+ }
+
+ $item_array = array(
+ 'url' => array(
+ 'loc' => $url,
+ 'lastmod' => jp_sitemap_datetime( $last_modified ),
+ ),
+ );
+
+ /**
+ * Filter sitemap URL item before rendering it as XML.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param array $tree Associative array representing sitemap URL element.
+ * @param int $post_id ID of the post being processed.
+ */
+ $item_array = apply_filters( 'jetpack_sitemap_url', $item_array, $post->ID );
+
+ return array(
+ 'xml' => $item_array,
+ 'last_modified' => $last_modified,
+ );
+ }
+
+ /**
+ * Construct the image sitemap url entry for a WP_Post of image type.
+ *
+ * @link http://www.sitemaps.org/protocol.html#urldef
+ *
+ * @access private
+ * @since 4.8.0
+ *
+ * @param WP_Post $post The image post to be processed.
+ *
+ * @return array
+ * @type array $xml An XML fragment representing the post URL.
+ * @type string $last_modified Date post was last modified.
+ */
+ private function image_post_to_sitemap_item( $post ) {
+
+ /**
+ * Filter condition to allow skipping specific image posts in the sitemap.
+ *
+ * @module sitemaps
+ *
+ * @since 4.8.0
+ *
+ * @param bool $skip Current boolean. False by default, so no post is skipped.
+ * @param WP_POST $post Current post object.
+ */
+ if ( apply_filters( 'jetpack_sitemap_image_skip_post', false, $post ) ) {
+ return array(
+ 'xml' => null,
+ 'last_modified' => null,
+ );
+ }
+
+ $url = wp_get_attachment_url( $post->ID );
+
+ // Do not include the image if the attached parent is not published.
+ // Unattached will be published. Otherwise, will inherit parent status.
+ if ( 'publish' !== get_post_status( $post ) ) {
+ return array(
+ 'xml' => null,
+ 'last_modified' => null,
+ );
+ }
+
+ $parent_url = get_permalink( get_post( $post->post_parent ) );
+ if ( '' == $parent_url ) { // WPCS: loose comparison ok.
+ $parent_url = get_permalink( $post );
+ }
+
+ $item_array = array(
+ 'url' => array(
+ 'loc' => $parent_url,
+ 'lastmod' => jp_sitemap_datetime( $post->post_modified_gmt ),
+ 'image:image' => array(
+ 'image:loc' => $url,
+ ),
+ ),
+ );
+
+ $item_array['url']['image:image']['image:title'] = $post->post_title;
+ $item_array['url']['image:image']['image:caption'] = $post->post_excerpt;
+
+ /**
+ * Filter associative array with data to build <url> node
+ * and its descendants for current post in image sitemap.
+ *
+ * @module sitemaps
+ *
+ * @since 4.8.0
+ *
+ * @param array $item_array Data to build parent and children nodes for current post.
+ * @param int $post_id Current image post ID.
+ */
+ $item_array = apply_filters(
+ 'jetpack_sitemap_image_sitemap_item',
+ $item_array,
+ $post->ID
+ );
+
+ return array(
+ 'xml' => $item_array,
+ 'last_modified' => $post->post_modified_gmt,
+ );
+ }
+
+ /**
+ * Construct the video sitemap url entry for a WP_Post of video type.
+ *
+ * @link http://www.sitemaps.org/protocol.html#urldef
+ * @link https://developers.google.com/webmasters/videosearch/sitemaps
+ *
+ * @access private
+ * @since 4.8.0
+ *
+ * @param WP_Post $post The video post to be processed.
+ *
+ * @return array
+ * @type array $xml An XML fragment representing the post URL.
+ * @type string $last_modified Date post was last modified.
+ */
+ private function video_post_to_sitemap_item( $post ) {
+
+ /**
+ * Filter condition to allow skipping specific image posts in the sitemap.
+ *
+ * @module sitemaps
+ *
+ * @since 4.8.0
+ *
+ * @param bool $skip Current boolean. False by default, so no post is skipped.
+ * @param WP_POST $post Current post object.
+ */
+ if ( apply_filters( 'jetpack_sitemap_video_skip_post', false, $post ) ) {
+ return array(
+ 'xml' => null,
+ 'last_modified' => null,
+ );
+ }
+
+ // Do not include the video if the attached parent is not published.
+ // Unattached will be published. Otherwise, will inherit parent status.
+ if ( 'publish' !== get_post_status( $post ) ) {
+ return array(
+ 'xml' => null,
+ 'last_modified' => null,
+ );
+ }
+
+ $parent_url = esc_url( get_permalink( get_post( $post->post_parent ) ) );
+ if ( '' == $parent_url ) { // WPCS: loose comparison ok.
+ $parent_url = esc_url( get_permalink( $post ) );
+ }
+
+ // Prepare the content like get_the_content_feed().
+ $content = $post->post_content;
+ /** This filter is already documented in core/wp-includes/post-template.php */
+ $content = apply_filters( 'the_content', $content );
+
+ /** This filter is already documented in core/wp-includes/feed.php */
+ $content = apply_filters( 'the_content_feed', $content, 'rss2' );
+
+ // Include thumbnails for VideoPress videos, use blank image for others
+ if ( 'complete' === get_post_meta( $post->ID, 'videopress_status', true ) && has_post_thumbnail( $post ) ) {
+ $video_thumbnail_url = get_the_post_thumbnail_url( $post );
+ } else {
+ /**
+ * Filter the thumbnail image used in the video sitemap for non-VideoPress videos.
+ *
+ * @since 7.2.0
+ *
+ * @param string $str Image URL.
+ */
+ $video_thumbnail_url = apply_filters( 'jetpack_video_sitemap_default_thumbnail', 'https://s0.wp.com/i/blank.jpg' );
+ }
+
+ $item_array = array(
+ 'url' => array(
+ 'loc' => $parent_url,
+ 'lastmod' => jp_sitemap_datetime( $post->post_modified_gmt ),
+ 'video:video' => array(
+ /** This filter is already documented in core/wp-includes/feed.php */
+ 'video:title' => apply_filters( 'the_title_rss', $post->post_title ),
+ 'video:thumbnail_loc' => esc_url( $video_thumbnail_url ),
+ 'video:description' => $content,
+ 'video:content_loc' => esc_url( wp_get_attachment_url( $post->ID ) ),
+ ),
+ ),
+ );
+
+ // TODO: Integrate with VideoPress here.
+ // cf. video:player_loc tag in video sitemap spec.
+
+ /**
+ * Filter associative array with data to build <url> node
+ * and its descendants for current post in video sitemap.
+ *
+ * @module sitemaps
+ *
+ * @since 4.8.0
+ *
+ * @param array $item_array Data to build parent and children nodes for current post.
+ * @param int $post_id Current video post ID.
+ */
+ $item_array = apply_filters(
+ 'jetpack_sitemap_video_sitemap_item',
+ $item_array,
+ $post->ID
+ );
+
+ return array(
+ 'xml' => $item_array,
+ 'last_modified' => $post->post_modified_gmt,
+ );
+ }
+
+ /**
+ * Construct the news sitemap url entry for a WP_Post.
+ *
+ * @link http://www.sitemaps.org/protocol.html#urldef
+ *
+ * @access private
+ * @since 4.8.0
+ *
+ * @param WP_Post $post The post to be processed.
+ *
+ * @return string An XML fragment representing the post URL.
+ */
+ private function post_to_news_sitemap_item( $post ) {
+
+ /**
+ * Filter condition to allow skipping specific posts in news sitemap.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param bool $skip Current boolean. False by default, so no post is skipped.
+ * @param WP_POST $post Current post object.
+ */
+ if ( apply_filters( 'jetpack_sitemap_news_skip_post', false, $post ) ) {
+ return array(
+ 'xml' => null,
+ );
+ }
+
+ $url = get_permalink( $post );
+
+ /*
+ * Spec requires the URL to be <=2048 bytes.
+ * In practice this constraint is unlikely to be violated.
+ */
+ if ( 2048 < strlen( $url ) ) {
+ $url = home_url() . '/?p=' . $post->ID;
+ }
+
+ /*
+ * Trim the locale to an ISO 639 language code as required by Google.
+ * Special cases are zh-cn (Simplified Chinese) and zh-tw (Traditional Chinese).
+ * @link http://www.loc.gov/standards/iso639-2/php/code_list.php
+ */
+ $language = strtolower( get_locale() );
+
+ if ( in_array( $language, array( 'zh_tw', 'zh_cn' ), true ) ) {
+ $language = str_replace( '_', '-', $language );
+ } else {
+ $language = preg_replace( '/(_.*)$/i', '', $language );
+ }
+
+ $item_array = array(
+ 'url' => array(
+ 'loc' => $url,
+ 'lastmod' => jp_sitemap_datetime( $post->post_modified_gmt ),
+ 'news:news' => array(
+ 'news:publication' => array(
+ 'news:name' => html_entity_decode( get_bloginfo( 'name' ) ),
+ 'news:language' => $language,
+ ),
+ /** This filter is already documented in core/wp-includes/feed.php */
+ 'news:title' => apply_filters( 'the_title_rss', $post->post_title ),
+ 'news:publication_date' => jp_sitemap_datetime( $post->post_date_gmt ),
+ 'news:genres' => 'Blog',
+ ),
+ ),
+ );
+
+ /**
+ * Filter associative array with data to build <url> node
+ * and its descendants for current post in news sitemap.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param array $item_array Data to build parent and children nodes for current post.
+ * @param int $post_id Current post ID.
+ */
+ $item_array = apply_filters(
+ 'jetpack_sitemap_news_sitemap_item',
+ $item_array,
+ $post->ID
+ );
+
+ return array(
+ 'xml' => $item_array,
+ );
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-constants.php b/plugins/jetpack/modules/sitemaps/sitemap-constants.php
new file mode 100644
index 00000000..e91a3341
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-constants.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * Sitemap-related constants.
+ *
+ * @package Jetpack
+ * @since 4.8.0
+ * @author Automattic
+ */
+
+/**
+ * Maximum size (in bytes) of a sitemap xml file.
+ * Max is 716800 = 700kb to avoid potential failures for default memcached limits (1MB)
+ *
+ * @link http://www.sitemaps.org/
+ * @since 4.8.0
+ */
+if ( ! defined( 'JP_SITEMAP_MAX_BYTES' ) ) {
+ define( 'JP_SITEMAP_MAX_BYTES', 716800 );
+}
+
+/**
+ * Maximum size (in url nodes) of a sitemap xml file.
+ * Per the spec, max value is 50000.
+ *
+ * @link http://www.sitemaps.org/
+ * @since 4.8.0
+ */
+if ( ! defined( 'JP_SITEMAP_MAX_ITEMS' ) ) {
+ define( 'JP_SITEMAP_MAX_ITEMS', 2000 );
+}
+
+/**
+ * Maximum size (in url nodes) of a news sitemap xml file.
+ * Per the spec, max value is 1000.
+ *
+ * @link https://support.google.com/news/publisher/answer/74288?hl=en
+ * @since 4.8.0
+ */
+if ( ! defined( 'JP_NEWS_SITEMAP_MAX_ITEMS' ) ) {
+ define( 'JP_NEWS_SITEMAP_MAX_ITEMS', 1000 );
+}
+
+/**
+ * Batch size for database queries.
+ *
+ * @since 4.8.0
+ */
+if ( ! defined( 'JP_SITEMAP_BATCH_SIZE' ) ) {
+ define( 'JP_SITEMAP_BATCH_SIZE', 50 );
+}
+
+/**
+ * Number of sitemap files to update on each run.
+ *
+ * @since 4.8.0
+ */
+if ( ! defined( 'JP_SITEMAP_UPDATE_SIZE' ) ) {
+ define( 'JP_SITEMAP_UPDATE_SIZE', 100 );
+}
+
+/**
+ * Number of seconds between sitemap updates.
+ *
+ * @since 4.8.0
+ */
+if ( ! defined( 'JP_SITEMAP_INTERVAL' ) ) {
+ define( 'JP_SITEMAP_INTERVAL', 12 * HOUR_IN_SECONDS );
+}
+
+/**
+ * Number of seconds to lock the sitemap state.
+ *
+ * @since 4.8.0
+ */
+if ( ! defined( 'JP_SITEMAP_LOCK_INTERVAL' ) ) {
+ define( 'JP_SITEMAP_LOCK_INTERVAL', 15 * MINUTE_IN_SECONDS );
+}
+
+/**
+ * Cache lifetime of news sitemap (in seconds).
+ *
+ * @since 4.8.0
+ */
+if ( ! defined( 'JP_NEWS_SITEMAP_INTERVAL' ) ) {
+ define( 'JP_NEWS_SITEMAP_INTERVAL', 12 * HOUR_IN_SECONDS );
+}
+
+/*
+ * These constants represent the types of various kinds of sitemaps.
+ * Note: these strings are used as 'post_types' in the database, and
+ * so must be at most 20 characters long.
+ */
+
+if ( ! defined( 'JP_MASTER_SITEMAP_TYPE' ) ) {
+ define( 'JP_MASTER_SITEMAP_TYPE', 'jp_sitemap_master' );
+}
+
+if ( ! defined( 'JP_PAGE_SITEMAP_TYPE' ) ) {
+ define( 'JP_PAGE_SITEMAP_TYPE', 'jp_sitemap' );
+}
+
+if ( ! defined( 'JP_PAGE_SITEMAP_INDEX_TYPE' ) ) {
+ define( 'JP_PAGE_SITEMAP_INDEX_TYPE', 'jp_sitemap_index' );
+}
+
+if ( ! defined( 'JP_IMAGE_SITEMAP_TYPE' ) ) {
+ define( 'JP_IMAGE_SITEMAP_TYPE', 'jp_img_sitemap' );
+}
+
+if ( ! defined( 'JP_IMAGE_SITEMAP_INDEX_TYPE' ) ) {
+ define( 'JP_IMAGE_SITEMAP_INDEX_TYPE', 'jp_img_sitemap_index' );
+}
+
+if ( ! defined( 'JP_VIDEO_SITEMAP_TYPE' ) ) {
+ define( 'JP_VIDEO_SITEMAP_TYPE', 'jp_vid_sitemap' );
+}
+
+if ( ! defined( 'JP_VIDEO_SITEMAP_INDEX_TYPE' ) ) {
+ define( 'JP_VIDEO_SITEMAP_INDEX_TYPE', 'jp_vid_sitemap_index' );
+}
+
+/**
+ * The name (with extension) of a sitemap file of the given
+ * type and number.
+ *
+ * @since 4.8.0
+ *
+ * @param string $type The sitemap type.
+ * @param string $number The sitemap number.
+ *
+ * @return string The filename.
+ */
+function jp_sitemap_filename( $type, $number = null ) {
+ if ( is_null( $number ) ) {
+ return "error-not-int-$type-$number.xml";
+ } elseif ( JP_MASTER_SITEMAP_TYPE === $type ) {
+ return 'sitemap.xml';
+ } elseif ( JP_PAGE_SITEMAP_TYPE === $type ) {
+ return "sitemap-$number.xml";
+ } elseif ( JP_PAGE_SITEMAP_INDEX_TYPE === $type ) {
+ return "sitemap-index-$number.xml";
+ } elseif ( JP_IMAGE_SITEMAP_TYPE === $type ) {
+ return "image-sitemap-$number.xml";
+ } elseif ( JP_IMAGE_SITEMAP_INDEX_TYPE === $type ) {
+ return "image-sitemap-index-$number.xml";
+ } elseif ( JP_VIDEO_SITEMAP_TYPE === $type ) {
+ return "video-sitemap-$number.xml";
+ } elseif ( JP_VIDEO_SITEMAP_INDEX_TYPE === $type ) {
+ return "video-sitemap-index-$number.xml";
+ } else {
+ return "error-bad-type-$type-$number.xml";
+ }
+}
+
+/**
+ * The index type corresponding to a sitemap type.
+ *
+ * @since 4.8.0
+ *
+ * @param string $type The sitemap type.
+ *
+ * @return string The index type.
+ */
+function jp_sitemap_index_type_of( $type ) {
+ if ( JP_PAGE_SITEMAP_TYPE === $type ) {
+ return JP_PAGE_SITEMAP_INDEX_TYPE;
+ } elseif ( JP_IMAGE_SITEMAP_TYPE === $type ) {
+ return JP_IMAGE_SITEMAP_INDEX_TYPE;
+ } elseif ( JP_VIDEO_SITEMAP_TYPE === $type ) {
+ return JP_VIDEO_SITEMAP_INDEX_TYPE;
+ } else {
+ return "error-bad-type-$type";
+ }
+}
+
+/**
+ * The sitemap type corresponding to an index type.
+ *
+ * @since 4.8.0
+ *
+ * @param string $type The index type.
+ *
+ * @return string The sitemap type.
+ */
+function jp_sitemap_child_type_of( $type ) {
+ if ( JP_PAGE_SITEMAP_INDEX_TYPE === $type ) {
+ return JP_PAGE_SITEMAP_TYPE;
+ } elseif ( JP_IMAGE_SITEMAP_INDEX_TYPE === $type ) {
+ return JP_IMAGE_SITEMAP_TYPE;
+ } elseif ( JP_VIDEO_SITEMAP_INDEX_TYPE === $type ) {
+ return JP_VIDEO_SITEMAP_TYPE;
+ } else {
+ return "error-bad-type-$type";
+ }
+}
+
+/**
+ * Convert '0000-00-00 00:00:00' to '0000-00-00T00:00:00Z'.
+ * Note that the input is assumed to be in UTC (a.k.a. GMT).
+ *
+ * @link https://www.w3.org/TR/NOTE-datetime
+ * @since 4.8.0
+ *
+ * @param string $datetime The timestamp to convert.
+ *
+ * @return string The converted timestamp.
+ */
+function jp_sitemap_datetime( $datetime ) {
+ $regex = '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/';
+
+ if ( preg_match( $regex, $datetime ) ) {
+ return str_replace( ' ', 'T', $datetime ) . 'Z';
+ } else {
+ return $datetime;
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-finder.php b/plugins/jetpack/modules/sitemaps/sitemap-finder.php
new file mode 100644
index 00000000..ae6335ce
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-finder.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * The functions in this class provide an API for handling
+ * sitemap related URIs.
+ *
+ * @package Jetpack
+ * @since 4.8.0
+ * @author Automattic
+ */
+
+/**
+ * The Jetpack_Sitemap_Finder object deals with constructing
+ * sitemap URIs.
+ *
+ * @since 4.8.0
+ */
+class Jetpack_Sitemap_Finder {
+
+ /**
+ * Construct the complete URL of a sitemap file. Depends on
+ * permalink settings.
+ *
+ * @access public
+ * @since 4.8.0
+ * @since 4.8.1 Call jetpack_sitemap_uri()
+ *
+ * @param string $filename The filename of the sitemap.
+ *
+ * @return string Complete URI of the given sitemap file.
+ */
+ public function construct_sitemap_url( $filename ) {
+ $url = jetpack_sitemap_uri( $filename );
+
+ if ( pathinfo( $filename, PATHINFO_EXTENSION ) === 'xsl' ) {
+ // strip scheme for sites where sitemap could be access via http or https
+ $url = preg_replace( '/^https?:/', '', $url );
+ }
+
+ return $url;
+ }
+
+ /**
+ * Path and query prefix of sitemap files. Depends on permalink
+ * settings.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @return string The path+query prefix.
+ */
+ public function the_jetpack_sitemap_path_and_query_prefix() {
+ global $wp_rewrite;
+
+ // Get path fragment from home_url().
+ $home = wp_parse_url( home_url() );
+ if ( isset( $home['path'] ) ) {
+ $home_path = $home['path'];
+ } else {
+ $home_path = '';
+ }
+
+ // Get additional path fragment from filter.
+ $location = Jetpack_Options::get_option_and_ensure_autoload(
+ 'jetpack_sitemap_location',
+ ''
+ );
+
+ if ( $wp_rewrite->using_index_permalinks() ) {
+ return $home_path . '/index.php' . $location . '/';
+ } elseif ( $wp_rewrite->using_permalinks() ) {
+ return $home_path . $location . '/';
+ } else {
+ return $home_path . $location . '/?jetpack-sitemap=';
+ }
+ }
+
+ /**
+ * Examine a path+query URI fragment looking for a sitemap request.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param string $raw_uri A URI (path+query only) to test for sitemap-ness.
+ *
+ * @return array @args {
+ * @type string $sitemap_name The recognized sitemap name (or null).
+ * }
+ */
+ public function recognize_sitemap_uri( $raw_uri ) {
+ // The path+query where sitemaps are served.
+ $sitemap_path = $this->the_jetpack_sitemap_path_and_query_prefix();
+
+ // A regex which detects $sitemap_path at the beginning of a string.
+ $path_regex = '/^' . preg_quote( $sitemap_path, '/' ) . '/';
+
+ // Check that the request URI begins with the sitemap path.
+ if ( preg_match( $path_regex, $raw_uri ) ) {
+ // Strip off the $sitemap_path and any trailing slash.
+ $stripped_uri = preg_replace( $path_regex, '', rtrim( $raw_uri, '/' ) );
+ } else {
+ $stripped_uri = '';
+ }
+
+ // Check that the stripped uri begins with one of the sitemap prefixes.
+ if ( preg_match( '/^sitemap|^image-s|^news-s|^video-s/', $stripped_uri ) ) {
+ $filename = $stripped_uri;
+ } else {
+ $filename = null;
+ }
+
+ return array(
+ 'sitemap_name' => $filename,
+ );
+ }
+
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-librarian.php b/plugins/jetpack/modules/sitemaps/sitemap-librarian.php
new file mode 100644
index 00000000..c923c952
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-librarian.php
@@ -0,0 +1,431 @@
+<?php
+/**
+ * Sitemaps are stored in the database using a custom table. This class
+ * provides a small API for storing and retrieving sitemap data so we can
+ * avoid lots of explicit SQL juggling while building sitemaps. This file
+ * also includes the SQL used to retrieve posts and images to be included
+ * in the sitemaps.
+ *
+ * @since 4.8.0
+ * @package Jetpack
+ */
+
+/* Ensure sitemap constants are available. */
+require_once dirname( __FILE__ ) . '/sitemap-constants.php';
+
+/**
+ * This object handles any database interaction required
+ * for sitemap generation.
+ *
+ * @since 4.8.0
+ */
+class Jetpack_Sitemap_Librarian {
+
+ /**
+ * Retrieve a single sitemap with given name and type.
+ * Returns null if no such sitemap exists.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param string $name Name of the sitemap to be retrieved.
+ * @param string $type Type of the sitemap to be retrieved.
+ *
+ * @return array $args {
+ * @type int $id ID number of the sitemap in the database.
+ * @type string $timestamp Most recent timestamp of the resources pointed to.
+ * @type string $name Name of the sitemap in the database.
+ * @type string $type Type of the sitemap in the database.
+ * @type string $text The content of the sitemap.
+ * }
+ */
+ public function read_sitemap_data( $name, $type ) {
+ $post_array = get_posts(
+ array(
+ 'numberposts' => 1,
+ 'title' => $name,
+ 'post_type' => $type,
+ 'post_status' => 'draft',
+ )
+ );
+
+ $the_post = array_shift( $post_array );
+
+ if ( null === $the_post ) {
+ return null;
+ } else {
+ return array(
+ 'id' => $the_post->ID,
+ 'timestamp' => $the_post->post_date,
+ 'name' => $the_post->post_title,
+ 'type' => $the_post->post_type,
+ 'text' => base64_decode( $the_post->post_content ),
+ );
+ }
+ }
+
+ /**
+ * Store a sitemap of given type and index in the database.
+ * Note that the timestamp is reencoded as 'Y-m-d H:i:s'.
+ *
+ * If a sitemap with that type and name does not exist, create it.
+ * If a sitemap with that type and name does exist, update it.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param string $index Index of the sitemap to be stored.
+ * @param string $type Type of the sitemap to be stored.
+ * @param string $contents Contents of the sitemap to be stored.
+ * @param string $timestamp Timestamp of the sitemap to be stored, in 'YYYY-MM-DD hh:mm:ss' format.
+ */
+ public function store_sitemap_data( $index, $type, $contents, $timestamp ) {
+ $name = jp_sitemap_filename( $type, $index );
+
+ $the_post = $this->read_sitemap_data( $name, $type );
+
+ if ( null === $the_post ) {
+ // Post does not exist.
+ wp_insert_post(
+ array(
+ 'post_title' => $name,
+ 'post_content' => base64_encode( $contents ),
+ 'post_type' => $type,
+ 'post_date' => date( 'Y-m-d H:i:s', strtotime( $timestamp ) ),
+ )
+ );
+ } else {
+ // Post does exist.
+ wp_insert_post(
+ array(
+ 'ID' => $the_post['id'],
+ 'post_title' => $name,
+ 'post_content' => base64_encode( $contents ),
+ 'post_type' => $type,
+ 'post_date' => date( 'Y-m-d H:i:s', strtotime( $timestamp ) ),
+ )
+ );
+ }
+ }
+
+ /**
+ * Delete a sitemap by name and type.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param string $name Row name.
+ * @param string $type Row type.
+ *
+ * @return bool 'true' if a row was deleted, 'false' otherwise.
+ */
+ public function delete_sitemap_data( $name, $type ) {
+ $the_post = $this->read_sitemap_data( $name, $type );
+
+ if ( null === $the_post ) {
+ return false;
+ } else {
+ wp_delete_post( $the_post['id'] );
+ return true;
+ }
+ }
+
+ /**
+ * Retrieve the contents of a sitemap with given name and type.
+ * If no such sitemap exists, return the empty string. Note that the
+ * returned string is run through wp_specialchars_decode.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param string $name Row name.
+ * @param string $type Row type.
+ *
+ * @return string Text of the specified sitemap, or the empty string.
+ */
+ public function get_sitemap_text( $name, $type ) {
+ $row = $this->read_sitemap_data( $name, $type );
+
+ if ( null === $row ) {
+ return '';
+ } else {
+ return $row['text'];
+ }
+ }
+
+ /**
+ * Delete numbered sitemaps named prefix-(p+1), prefix-(p+2), ...
+ * until the first nonexistent sitemap is found.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param int $position Number before the first sitemap to be deleted.
+ * @param string $type Sitemap type.
+ */
+ public function delete_numbered_sitemap_rows_after( $position, $type ) {
+ $any_left = true;
+
+ while ( true === $any_left ) {
+ $position++;
+ $name = jp_sitemap_filename( $type, $position );
+ $any_left = $this->delete_sitemap_data( $name, $type );
+ }
+ }
+
+ /**
+ * Deletes all stored sitemap data.
+ *
+ * @access public
+ * @since 4.8.0
+ */
+ public function delete_all_stored_sitemap_data() {
+ $this->delete_sitemap_type_data( JP_MASTER_SITEMAP_TYPE );
+ $this->delete_sitemap_type_data( JP_PAGE_SITEMAP_TYPE );
+ $this->delete_sitemap_type_data( JP_PAGE_SITEMAP_INDEX_TYPE );
+ $this->delete_sitemap_type_data( JP_IMAGE_SITEMAP_TYPE );
+ $this->delete_sitemap_type_data( JP_IMAGE_SITEMAP_INDEX_TYPE );
+ $this->delete_sitemap_type_data( JP_VIDEO_SITEMAP_TYPE );
+ $this->delete_sitemap_type_data( JP_VIDEO_SITEMAP_INDEX_TYPE );
+ }
+
+ /**
+ * Deletes all sitemap data of specific type
+ *
+ * @access protected
+ * @since 5.3.0
+ *
+ * @param String $type Type of sitemap.
+ */
+ protected function delete_sitemap_type_data( $type ) {
+ $ids = get_posts(
+ array(
+ 'post_type' => $type,
+ 'post_status' => 'draft',
+ 'fields' => 'ids',
+ )
+ );
+
+ foreach ( $ids as $id ) {
+ wp_trash_post( $id );
+ }
+ }
+
+ /**
+ * Retrieve an array of sitemap rows (of a given type) sorted by ID.
+ *
+ * Returns the smallest $num_posts sitemap rows (measured by ID)
+ * of the given type which are larger than $from_id.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param string $type Type of the sitemap rows to retrieve.
+ * @param int $from_id Greatest lower bound of retrieved sitemap post IDs.
+ * @param int $num_posts Largest number of sitemap posts to retrieve.
+ *
+ * @return array The sitemaps, as an array of associative arrays.
+ */
+ public function query_sitemaps_after_id( $type, $from_id, $num_posts ) {
+ global $wpdb;
+
+ return $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT *
+ FROM $wpdb->posts
+ WHERE post_type=%s
+ AND post_status=%s
+ AND ID>%d
+ ORDER BY ID ASC
+ LIMIT %d;",
+ $type,
+ 'draft',
+ $from_id,
+ $num_posts
+ ),
+ ARRAY_A
+ ); // WPCS: db call ok; no-cache ok.
+ }
+
+ /**
+ * Retrieve an array of posts sorted by ID.
+ *
+ * More precisely, returns the smallest $num_posts posts
+ * (measured by ID) which are larger than $from_id.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param int $from_id Greatest lower bound of retrieved post IDs.
+ * @param int $num_posts Largest number of posts to retrieve.
+ *
+ * @return array The posts.
+ */
+ public function query_posts_after_id( $from_id, $num_posts ) {
+ global $wpdb;
+
+ // Get the list of post types to include and prepare for query.
+ $post_types = Jetpack_Options::get_option_and_ensure_autoload(
+ 'jetpack_sitemap_post_types',
+ array( 'page', 'post' )
+ );
+ foreach ( (array) $post_types as $i => $post_type ) {
+ $post_types[ $i ] = $wpdb->prepare( '%s', $post_type );
+ }
+ $post_types_list = join( ',', $post_types );
+
+ return $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT *
+ FROM $wpdb->posts
+ WHERE post_status='publish'
+ AND post_type IN ($post_types_list)
+ AND ID>%d
+ ORDER BY ID ASC
+ LIMIT %d;",
+ $from_id,
+ $num_posts
+ )
+ ); // WPCS: db call ok; no-cache ok.
+ }
+
+ /**
+ * Get the most recent timestamp among approved comments for the given post_id.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param int $post_id Post identifier.
+ *
+ * @return int Timestamp in 'Y-m-d h:i:s' format (UTC) of the most recent comment on the given post, or null if no such comments exist.
+ */
+ public function query_latest_approved_comment_time_on_post( $post_id ) {
+ global $wpdb;
+
+ return $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT MAX(comment_date_gmt)
+ FROM $wpdb->comments
+ WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type=''",
+ $post_id
+ )
+ );
+ }
+
+ /**
+ * Retrieve an array of image posts sorted by ID.
+ *
+ * More precisely, returns the smallest $num_posts image posts
+ * (measured by ID) which are larger than $from_id.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param int $from_id Greatest lower bound of retrieved image post IDs.
+ * @param int $num_posts Largest number of image posts to retrieve.
+ *
+ * @return array The posts.
+ */
+ public function query_images_after_id( $from_id, $num_posts ) {
+ global $wpdb;
+
+ return $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT *
+ FROM $wpdb->posts
+ WHERE post_type='attachment'
+ AND post_mime_type LIKE %s
+ AND ID>%d
+ ORDER BY ID ASC
+ LIMIT %d;",
+ 'image/%',
+ $from_id,
+ $num_posts
+ )
+ ); // WPCS: db call ok; no-cache ok.
+ }
+
+ /**
+ * Retrieve an array of video posts sorted by ID.
+ *
+ * More precisely, returns the smallest $num_posts video posts
+ * (measured by ID) which are larger than $from_id.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param int $from_id Greatest lower bound of retrieved video post IDs.
+ * @param int $num_posts Largest number of video posts to retrieve.
+ *
+ * @return array The posts.
+ */
+ public function query_videos_after_id( $from_id, $num_posts ) {
+ global $wpdb;
+
+ return $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT *
+ FROM $wpdb->posts
+ WHERE post_type='attachment'
+ AND post_mime_type LIKE %s
+ AND ID>%d
+ ORDER BY ID ASC
+ LIMIT %d;",
+ 'video/%',
+ $from_id,
+ $num_posts
+ )
+ ); // WPCS: db call ok; no-cache ok.
+ }
+
+ /**
+ * Retrieve an array of published posts from the last 2 days.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param int $num_posts Largest number of posts to retrieve.
+ *
+ * @return array The posts.
+ */
+ public function query_most_recent_posts( $num_posts ) {
+ global $wpdb;
+
+ $two_days_ago = date( 'Y-m-d', strtotime( '-2 days' ) );
+
+ /**
+ * Filter post types to be included in news sitemap.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param array $post_types Array with post types to include in news sitemap.
+ */
+ $post_types = apply_filters(
+ 'jetpack_sitemap_news_sitemap_post_types',
+ array( 'page', 'post' )
+ );
+
+ foreach ( (array) $post_types as $i => $post_type ) {
+ $post_types[ $i ] = $wpdb->prepare( '%s', $post_type );
+ }
+
+ $post_types_list = join( ',', $post_types );
+
+ return $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT *
+ FROM $wpdb->posts
+ WHERE post_status='publish'
+ AND post_date >= '%s'
+ AND post_type IN ($post_types_list)
+ ORDER BY post_date DESC
+ LIMIT %d;",
+ $two_days_ago,
+ $num_posts
+ )
+ ); // WPCS: db call ok; no-cache ok.
+ }
+
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-logger.php b/plugins/jetpack/modules/sitemaps/sitemap-logger.php
new file mode 100644
index 00000000..1173aa79
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-logger.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * A message logger for the Jetpack Sitemap module.
+ *
+ * @package Jetpack
+ * @since 4.8.0
+ */
+
+/**
+ * Handles logging errors and debug messages for sitemap generator.
+ *
+ * A Jetpack_Sitemap_Logger object keeps track of its birth time as well
+ * as a "unique" ID string. Calling the report() method writes a message
+ * to the PHP error log as well as the ID string for easier grepping.
+ *
+ * @since 4.8.0
+ */
+class Jetpack_Sitemap_Logger {
+ /**
+ * A unique-ish string for each logger, enabling us to grep
+ * for the messages written by an individual generation phase.
+ *
+ * @access private
+ * @since 4.8.0
+ * @var string $key The key string.
+ */
+ private $key;
+
+ /**
+ * The birth time of this object in microseconds.
+ *
+ * @access private
+ * @since 4.8.0
+ * @var int $starttime The birth time.
+ */
+ private $starttime;
+
+ /**
+ * Initializes a new logger object.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param string $message An optional message string to be written to the debug log on initialization.
+ */
+ public function __construct( $message = null ) {
+ $this->key = wp_generate_password( 5, false );
+ $this->starttime = microtime( true );
+ if ( ! is_null( $message ) ) {
+ $this->report( $message );
+ }
+ }
+
+ /**
+ * Writes a string to the debug log, including the logger's ID string.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param string $message The string to be written to the log.
+ * @param boolean $is_error If true, $message will be logged even if JETPACK_DEV_DEBUG is not enabled.
+ */
+ public function report( $message, $is_error = false ) {
+ $message = 'jp-sitemap-' . $this->key . ': ' . $message;
+ if ( ! ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
+ return;
+ }
+ if ( ! $is_error && ! ( defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG ) ) {
+ return;
+ }
+ error_log( $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ }
+
+ /**
+ * Writes the elapsed lifetime of the logger to the debug log, with an optional message.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param string $message The optional message string. Default is the empty string.
+ */
+ public function time( $message = '' ) {
+ $time = round( microtime( true ) - $this->starttime, 3 );
+ $this->report( $message . ' ' . $time . ' seconds elapsed.' );
+ }
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-state.php b/plugins/jetpack/modules/sitemaps/sitemap-state.php
new file mode 100644
index 00000000..2157628e
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-state.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Abstract sitemap generation state class.
+ *
+ * @package Jetpack
+ * @since 4.8.0
+ * @author Automattic
+ */
+
+/* Include standard constants and librarian. */
+require_once dirname( __FILE__ ) . '/sitemap-constants.php';
+require_once dirname( __FILE__ ) . '/sitemap-librarian.php';
+
+if ( defined( 'WP_DEBUG' ) && ( true === WP_DEBUG ) ) {
+ require_once dirname( __FILE__ ) . '/sitemap-logger.php';
+}
+
+/**
+ * This class provides an interface for storing and retrieving
+ * the state of a sitemap generation phase. Whenever the builder
+ * wants to build a new sitemap page, it uses this class to see
+ * what the current state of the sitemap is. The lock is stored
+ * as a transient with max lifetime of 15 minutes; this way if our
+ * builder times out before unlocking the state, the lock will expire
+ * before the builder tries again.
+ *
+ * @since 4.8.0
+ */
+class Jetpack_Sitemap_State {
+
+ /**
+ * Initial state for the sitemap generator.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param string $type The initial sitemap type.
+ *
+ * @return array $args {
+ * @type string sitemap-type The type of sitemap to be generated.
+ * @type int last-added The largest index to be added to a generated sitemap page.
+ * @type int number The index of the last sitemap to be generated.
+ * @type string last-modified The latest timestamp seen.
+ * @type array max The latest index of each sitemap type seen.
+ * }
+ */
+ private static function initial( $type = JP_PAGE_SITEMAP_TYPE ) {
+ return array(
+ 'sitemap-type' => $type,
+ 'last-added' => 0,
+ 'number' => 0,
+ 'last-modified' => '1970-01-01 00:00:00',
+ 'max' => array(),
+ );
+ }
+
+ /**
+ * Reset the sitemap state.
+ *
+ * @param string $type The initial sitemap type.
+ *
+ * @access public
+ * @since 4.8.0
+ */
+ public static function reset( $type ) {
+ delete_transient( 'jetpack-sitemap-state-lock' );
+ update_option(
+ 'jetpack-sitemap-state',
+ self::initial( $type )
+ );
+ }
+
+ /**
+ * Store a sitemap state, and unlock it.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param array $state Array of the Sitemap state details.
+ * @type string sitemap-type The type of sitemap to be generated.
+ * @type int last-added The largest index to be added to a generated sitemap page.
+ * @type int number The index of the last sitemap to be generated.
+ * @type string last-modified The latest timestamp seen.
+ */
+ public static function check_in( $state ) {
+ // Get the old max value.
+ $sitemap_old = get_option( 'jetpack-sitemap-state', self::initial() );
+ $state['max'] = $sitemap_old['max'];
+
+ // Update the max value of the current type.
+ $state['max'][ $state['sitemap-type'] ]['number'] = $state['number'];
+ $state['max'][ $state['sitemap-type'] ]['lastmod'] = $state['last-modified'];
+
+ update_option( 'jetpack-sitemap-state', $state );
+ }
+
+ /**
+ * Unlock the sitemap state.
+ *
+ * @access public
+ * @since 4.8.0
+ */
+ public static function unlock() {
+ delete_transient( 'jetpack-sitemap-state-lock' );
+ }
+
+ /**
+ * Read the stored sitemap state. Returns false if the state is locked.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @return bool|array $args {
+ * @type string sitemap-type The type of sitemap to be generated.
+ * @type int last-added The largest index to be added to a generated sitemap page.
+ * @type int number The index of the last sitemap to be generated.
+ * @type string last-modified The latest timestamp seen.
+ * @type array max The latest index of each sitemap type seen.
+ * }
+ */
+ public static function check_out() {
+ // See if the state is locked.
+ if ( true === get_transient( 'jetpack-sitemap-state-lock' ) ) {
+ // If it is, return false.
+ return false;
+ } else {
+ // Otherwise, lock the state for 15 minutes and then return it.
+ set_transient( 'jetpack-sitemap-state-lock', true, JP_SITEMAP_LOCK_INTERVAL );
+ return get_option( 'jetpack-sitemap-state', self::initial() );
+ }
+ }
+
+ /**
+ * Delete the stored state and lock.
+ *
+ * @access public
+ * @since 4.8.0
+ */
+ public static function delete() {
+ delete_transient( 'jetpack-sitemap-state-lock' );
+ delete_option( 'jetpack-sitemap-state' );
+ }
+
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemap-stylist.php b/plugins/jetpack/modules/sitemaps/sitemap-stylist.php
new file mode 100644
index 00000000..ce883d7f
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemap-stylist.php
@@ -0,0 +1,783 @@
+<?php
+/**
+ * The XSL used to style sitemaps is essentially a bunch of
+ * static strings. This class handles the construction of
+ * those strings.
+ *
+ * @package Jetpack
+ * @since 4.8.0
+ */
+
+/**
+ * Builds the XSL files required by Jetpack_Sitemap_Manager.
+ *
+ * @since 4.8.0
+ */
+class Jetpack_Sitemap_Stylist {
+
+ /**
+ * Convert named entities, strip all HTML except anchor tags,
+ * and interpolate with vsprintf. This is a helper function
+ * for all the internationalized UI strings in this class
+ * which have to include URLs.
+ *
+ * Note that $url_array should be indexed by integers like so:
+ *
+ * array(
+ * 1 => 'example.com',
+ * 2 => 'example.org',
+ * );
+ *
+ * Then '%1$s' in the format string will substitute 'example.com'
+ * and '%2$s' will substitute 'example.org'.
+ *
+ * @access private
+ * @since 4.8.0
+ * @link http://php.net/manual/en/function.vsprintf.php Format string documentation.
+ *
+ * @param string $format A vsprintf-style format string to be sanitized.
+ * @param array $url_array The string substitution array to be passed to vsprintf.
+ *
+ * @return string The sanitized string.
+ */
+ private static function sanitize_with_links( $format, $url_array ) {
+ return vsprintf(
+ wp_kses(
+ ent2ncr( $format ),
+ array(
+ 'a' => array(
+ 'href' => true,
+ 'title' => true,
+ ),
+ )
+ ),
+ $url_array
+ );
+ }
+
+ /**
+ * Returns the xsl of a sitemap xml file as a string.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @return string The contents of the xsl file.
+ */
+ public static function sitemap_xsl() {
+ $title = esc_html( ent2ncr( __( 'XML Sitemap', 'jetpack' ) ) );
+ $header_url = esc_html( ent2ncr( __( 'URL', 'jetpack' ) ) );
+ $header_lastmod = esc_html( ent2ncr( __( 'Last Modified', 'jetpack' ) ) );
+
+ $description = self::sanitize_with_links(
+ __(
+ 'This is an XML Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'http://jetpack.com/',
+ 2 => 'https://www.google.com/',
+ 3 => 'https://www.bing.com/',
+ )
+ );
+
+ $more_info = self::sanitize_with_links(
+ __(
+ 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'http://sitemaps.org',
+ )
+ );
+
+ $generated_by = self::sanitize_with_links(
+ __(
+ 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'https://jetpack.com',
+ )
+ );
+
+ $css = self::sitemap_xsl_css();
+
+ return <<<XSL
+<?xml version='1.0' encoding='UTF-8'?>
+<xsl:stylesheet version='2.0'
+ xmlns:html='http://www.w3.org/TR/REC-html40'
+ xmlns:sitemap='http://www.sitemaps.org/schemas/sitemap/0.9'
+ xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
+<xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/>
+<xsl:template match="/">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>$title</title>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>
+ <style type='text/css'>
+$css
+ </style>
+</head>
+<body>
+ <div id='description'>
+ <h1>$title</h1>
+ <p>$description</p>
+ <p>$more_info</p>
+ </div>
+ <div id='content'>
+ <!-- <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> -->
+ <table>
+ <tr>
+ <th>#</th>
+ <th>$header_url</th>
+ <th>$header_lastmod</th>
+ </tr>
+ <xsl:for-each select="sitemap:urlset/sitemap:url">
+ <tr>
+ <xsl:choose>
+ <xsl:when test='position() mod 2 != 1'>
+ <xsl:attribute name="class">odd</xsl:attribute>
+ </xsl:when>
+ </xsl:choose>
+ <td>
+ <xsl:value-of select = "position()" />
+ </td>
+ <td>
+ <xsl:variable name='itemURL'>
+ <xsl:value-of select='sitemap:loc'/>
+ </xsl:variable>
+ <a href='{\$itemURL}'>
+ <xsl:value-of select='sitemap:loc'/>
+ </a>
+ </td>
+ <td>
+ <xsl:value-of select='sitemap:lastmod'/>
+ </td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ </div>
+ <div id='footer'>
+ <p>$generated_by</p>
+ </div>
+</body>
+</html>
+</xsl:template>
+</xsl:stylesheet>\n
+XSL;
+ }
+
+ /**
+ * Returns the xsl of a sitemap index xml file as a string.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @return string The contents of the xsl file.
+ */
+ public static function sitemap_index_xsl() {
+ $title = esc_html( ent2ncr( __( 'XML Sitemap Index', 'jetpack' ) ) );
+ $header_url = esc_html( ent2ncr( __( 'Sitemap URL', 'jetpack' ) ) );
+ $header_lastmod = esc_html( ent2ncr( __( 'Last Modified', 'jetpack' ) ) );
+
+ $description = self::sanitize_with_links(
+ __(
+ 'This is an XML Sitemap Index generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'http://jetpack.com/',
+ 2 => 'https://www.google.com/',
+ 3 => 'https://www.bing.com/',
+ )
+ );
+
+ if ( current_user_can( 'manage_options' ) ) {
+ $next = human_time_diff( wp_next_scheduled( 'jp_sitemap_cron_hook' ) );
+ /* translators: %s is a human_time_diff until next sitemap generation. */
+ $no_nodes_warning = sprintf( __( 'No sitemap found. The system will try to build it again in %s.', 'jetpack' ), $next );
+ } else {
+ $no_nodes_warning = '';
+ }
+
+ $more_info = self::sanitize_with_links(
+ __(
+ 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'http://sitemaps.org',
+ )
+ );
+
+ $generated_by = self::sanitize_with_links(
+ __(
+ 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'https://jetpack.com',
+ )
+ );
+
+ $css = self::sitemap_xsl_css();
+
+ return <<<XSL
+<?xml version='1.0' encoding='UTF-8'?>
+<xsl:stylesheet version='2.0'
+ xmlns:html='http://www.w3.org/TR/REC-html40'
+ xmlns:sitemap='http://www.sitemaps.org/schemas/sitemap/0.9'
+ xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
+<xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/>
+<xsl:template match="/">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>$title</title>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>
+ <style type='text/css'>
+$css
+ </style>
+</head>
+<body>
+ <div id='description'>
+ <h1>$title</h1>
+ <xsl:choose>
+ <xsl:when test='not(sitemap:sitemapindex/sitemap:sitemap)'>
+ <p><strong>$no_nodes_warning</strong></p>
+ </xsl:when>
+ </xsl:choose>
+ <p>$description</p>
+ <p>$more_info</p>
+ </div>
+ <div id='content'>
+ <table>
+ <tr>
+ <th>#</th>
+ <th>$header_url</th>
+ <th>$header_lastmod</th>
+ </tr>
+ <xsl:for-each select='sitemap:sitemapindex/sitemap:sitemap'>
+ <tr>
+ <xsl:choose>
+ <xsl:when test='position() mod 2 != 1'>
+ <xsl:attribute name="class">odd</xsl:attribute>
+ </xsl:when>
+ </xsl:choose>
+ <td>
+ <xsl:value-of select = "position()" />
+ </td>
+ <td>
+ <xsl:variable name='itemURL'>
+ <xsl:value-of select='sitemap:loc'/>
+ </xsl:variable>
+ <a href='{\$itemURL}'>
+ <xsl:value-of select='sitemap:loc'/>
+ </a>
+ </td>
+ <td>
+ <xsl:value-of select='sitemap:lastmod'/>
+ </td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ </div>
+ <div id='footer'>
+ <p>$generated_by</p>
+ </div>
+</body>
+</html>
+</xsl:template>
+</xsl:stylesheet>\n
+XSL;
+ }
+
+ /**
+ * Returns the xsl of an image sitemap xml file as a string.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @return string The contents of the xsl file.
+ */
+ public static function image_sitemap_xsl() {
+ $title = esc_html( ent2ncr( __( 'XML Image Sitemap', 'jetpack' ) ) );
+ $header_url = esc_html( ent2ncr( __( 'Page URL', 'jetpack' ) ) );
+ $header_image_url = esc_html( ent2ncr( __( 'Image URL', 'jetpack' ) ) );
+ $header_thumbnail = esc_html( ent2ncr( __( 'Thumbnail', 'jetpack' ) ) );
+ $header_title = esc_html( ent2ncr( __( 'Title', 'jetpack' ) ) );
+ $header_lastmod = esc_html( ent2ncr( __( 'Last Modified', 'jetpack' ) ) );
+ $header_caption = esc_html( ent2ncr( __( 'Caption', 'jetpack' ) ) );
+
+ $description = self::sanitize_with_links(
+ __(
+ 'This is an XML Image Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'http://jetpack.com/',
+ 2 => 'https://www.google.com/',
+ 3 => 'https://www.bing.com/',
+ )
+ );
+
+ $more_info = self::sanitize_with_links(
+ __(
+ 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'http://sitemaps.org',
+ )
+ );
+
+ $generated_by = self::sanitize_with_links(
+ __(
+ 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'https://jetpack.com',
+ )
+ );
+
+ $css = self::sitemap_xsl_css();
+
+ return <<<XSL
+<?xml version='1.0' encoding='UTF-8'?>
+<xsl:stylesheet version='2.0'
+ xmlns:html='http://www.w3.org/TR/REC-html40'
+ xmlns:sitemap='http://www.sitemaps.org/schemas/sitemap/0.9'
+ xmlns:image='http://www.google.com/schemas/sitemap-image/1.1'
+ xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
+<xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/>
+<xsl:template match="/">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>$title</title>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>
+ <style type='text/css'>
+$css
+ </style>
+</head>
+<body>
+ <div id='description'>
+ <h1>$title</h1>
+ <p>$description</p>
+ <p>$more_info</p>
+ </div>
+ <div id='content'>
+ <!-- <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> -->
+ <table>
+ <tr>
+ <th>#</th>
+ <th>$header_url</th>
+ <th>$header_image_url</th>
+ <th>$header_title</th>
+ <th>$header_caption</th>
+ <th>$header_lastmod</th>
+ <th>$header_thumbnail</th>
+ </tr>
+ <xsl:for-each select="sitemap:urlset/sitemap:url">
+ <tr>
+ <xsl:choose>
+ <xsl:when test='position() mod 2 != 1'>
+ <xsl:attribute name="class">odd</xsl:attribute>
+ </xsl:when>
+ </xsl:choose>
+ <td>
+ <xsl:value-of select = "position()" />
+ </td>
+ <td>
+ <xsl:variable name='pageURL'>
+ <xsl:value-of select='sitemap:loc'/>
+ </xsl:variable>
+ <a href='{\$pageURL}'>
+ <xsl:value-of select='sitemap:loc'/>
+ </a>
+ </td>
+ <xsl:variable name='itemURL'>
+ <xsl:value-of select='image:image/image:loc'/>
+ </xsl:variable>
+ <td>
+ <a href='{\$itemURL}'>
+ <xsl:value-of select='image:image/image:loc'/>
+ </a>
+ </td>
+ <td>
+ <xsl:value-of select='image:image/image:title'/>
+ </td>
+ <td>
+ <xsl:value-of select='image:image/image:caption'/>
+ </td>
+ <td>
+ <xsl:value-of select='sitemap:lastmod'/>
+ </td>
+ <td>
+ <a href='{\$itemURL}'>
+ <img class='thumbnail' src='{\$itemURL}'/>
+ </a>
+ </td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ </div>
+ <div id='footer'>
+ <p>$generated_by</p>
+ </div>
+</body>
+</html>
+</xsl:template>
+</xsl:stylesheet>\n
+XSL;
+ }
+
+ /**
+ * Returns the xsl of a video sitemap xml file as a string.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @return string The contents of the xsl file.
+ */
+ public static function video_sitemap_xsl() {
+ $title = esc_html( ent2ncr( __( 'XML Video Sitemap', 'jetpack' ) ) );
+ $header_url = esc_html( ent2ncr( __( 'Page URL', 'jetpack' ) ) );
+ $header_image_url = esc_html( ent2ncr( __( 'Video URL', 'jetpack' ) ) );
+ $header_thumbnail = esc_html( ent2ncr( __( 'Thumbnail', 'jetpack' ) ) );
+ $header_title = esc_html( ent2ncr( __( 'Title', 'jetpack' ) ) );
+ $header_lastmod = esc_html( ent2ncr( __( 'Last Modified', 'jetpack' ) ) );
+ $header_description = esc_html( ent2ncr( __( 'Description', 'jetpack' ) ) );
+
+ $description = self::sanitize_with_links(
+ __(
+ 'This is an XML Video Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'http://jetpack.com/',
+ 2 => 'https://www.google.com/',
+ 3 => 'https://www.bing.com/',
+ )
+ );
+
+ $more_info = self::sanitize_with_links(
+ __(
+ 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'http://sitemaps.org',
+ )
+ );
+
+ $generated_by = self::sanitize_with_links(
+ __(
+ 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'https://jetpack.com',
+ )
+ );
+
+ $css = self::sitemap_xsl_css();
+
+ return <<<XSL
+<?xml version='1.0' encoding='UTF-8'?>
+<xsl:stylesheet version='2.0'
+ xmlns:html='http://www.w3.org/TR/REC-html40'
+ xmlns:sitemap='http://www.sitemaps.org/schemas/sitemap/0.9'
+ xmlns:video='http://www.google.com/schemas/sitemap-video/1.1'
+ xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
+<xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/>
+<xsl:template match="/">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>$title</title>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>
+ <style type='text/css'>
+$css
+ </style>
+</head>
+<body>
+ <div id='description'>
+ <h1>$title</h1>
+ <p>$description</p>
+ <p>$more_info</p>
+ </div>
+ <div id='content'>
+ <!-- <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> -->
+ <table>
+ <tr>
+ <th>#</th>
+ <th>$header_url</th>
+ <th>$header_image_url</th>
+ <th>$header_title</th>
+ <th>$header_description</th>
+ <th>$header_lastmod</th>
+ <th>$header_thumbnail</th>
+ </tr>
+ <xsl:for-each select="sitemap:urlset/sitemap:url">
+ <tr>
+ <xsl:choose>
+ <xsl:when test='position() mod 2 != 1'>
+ <xsl:attribute name="class">odd</xsl:attribute>
+ </xsl:when>
+ </xsl:choose>
+ <td>
+ <xsl:value-of select = "position()" />
+ </td>
+ <td>
+ <xsl:variable name='pageURL'>
+ <xsl:value-of select='sitemap:loc'/>
+ </xsl:variable>
+ <a href='{\$pageURL}'>
+ <xsl:value-of select='sitemap:loc'/>
+ </a>
+ </td>
+ <xsl:variable name='itemURL'>
+ <xsl:value-of select='video:video/video:content_loc'/>
+ </xsl:variable>
+ <td>
+ <a href='{\$itemURL}'>
+ <xsl:value-of select='video:video/video:content_loc'/>
+ </a>
+ </td>
+ <td>
+ <xsl:value-of select='video:video/video:title'/>
+ </td>
+ <td>
+ <xsl:value-of select='video:video/video:description'/>
+ </td>
+ <td>
+ <xsl:value-of select='sitemap:lastmod'/>
+ </td>
+ <td>
+ <xsl:variable name='thumbURL'>
+ <xsl:value-of select='video:video/video:thumbnail_loc'/>
+ </xsl:variable>
+ <a href='{\$thumbURL}'>
+ <img class='thumbnail' src='{\$thumbURL}'/>
+ </a>
+ </td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ </div>
+ <div id='footer'>
+ <p>$generated_by</p>
+ </div>
+</body>
+</html>
+</xsl:template>
+</xsl:stylesheet>\n
+XSL;
+ }
+
+ /**
+ * Returns the xsl of a news sitemap xml file as a string.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @return string The contents of the xsl file.
+ */
+ public static function news_sitemap_xsl() {
+ $title = esc_html( ent2ncr( __( 'XML News Sitemap', 'jetpack' ) ) );
+ $header_url = esc_html( ent2ncr( __( 'Page URL', 'jetpack' ) ) );
+ $header_title = esc_html( ent2ncr( __( 'Title', 'jetpack' ) ) );
+ $header_pubdate = esc_html( ent2ncr( __( 'Publication Date', 'jetpack' ) ) );
+
+ $description = self::sanitize_with_links(
+ __(
+ 'This is an XML News Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'http://jetpack.com/',
+ 2 => 'https://www.google.com/',
+ 3 => 'https://www.bing.com/',
+ )
+ );
+
+ $more_info = self::sanitize_with_links(
+ __(
+ 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'http://sitemaps.org',
+ )
+ );
+
+ $generated_by = self::sanitize_with_links(
+ __(
+ 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>',
+ 'jetpack'
+ ),
+ array(
+ 1 => 'https://jetpack.com',
+ )
+ );
+
+ $css = self::sitemap_xsl_css();
+
+ return <<<XSL
+<?xml version='1.0' encoding='UTF-8'?>
+<xsl:stylesheet version='2.0'
+ xmlns:html='http://www.w3.org/TR/REC-html40'
+ xmlns:sitemap='http://www.sitemaps.org/schemas/sitemap/0.9'
+ xmlns:news='http://www.google.com/schemas/sitemap-news/0.9'
+ xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
+<xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/>
+<xsl:template match="/">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>$title</title>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>
+ <style type='text/css'>
+$css
+ </style>
+</head>
+<body>
+ <div id='description'>
+ <h1>$title</h1>
+ <p>$description</p>
+ <p>$more_info</p>
+ </div>
+ <div id='content'>
+ <!-- <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> -->
+ <table>
+ <tr>
+ <th>#</th>
+ <th>$header_url</th>
+ <th>$header_title</th>
+ <th>$header_pubdate</th>
+ </tr>
+ <xsl:for-each select="sitemap:urlset/sitemap:url">
+ <tr>
+ <xsl:choose>
+ <xsl:when test='position() mod 2 != 1'>
+ <xsl:attribute name="class">odd</xsl:attribute>
+ </xsl:when>
+ </xsl:choose>
+ <td>
+ <xsl:value-of select = "position()" />
+ </td>
+ <xsl:variable name='pageURL'>
+ <xsl:value-of select='sitemap:loc'/>
+ </xsl:variable>
+ <td>
+ <a href='{\$pageURL}'>
+ <xsl:value-of select='sitemap:loc'/>
+ </a>
+ </td>
+ <td>
+ <a href='{\$pageURL}'>
+ <xsl:value-of select='news:news/news:title'/>
+ </a>
+ </td>
+ <td>
+ <xsl:value-of select='news:news/news:publication_date'/>
+ </td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ </div>
+ <div id='footer'>
+ <p>$generated_by</p>
+ </div>
+</body>
+</html>
+</xsl:template>
+</xsl:stylesheet>\n
+XSL;
+ }
+
+ /**
+ * The CSS to be included in sitemap xsl stylesheets;
+ * factored out for uniformity.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @return string The CSS.
+ */
+ public static function sitemap_xsl_css() {
+ return <<<CSS
+ body {
+ font: 14px 'Open Sans', Helvetica, Arial, sans-serif;
+ margin: 0;
+ }
+
+ a {
+ color: #3498db;
+ text-decoration: none;
+ }
+
+ h1 {
+ margin: 0;
+ }
+
+ #description {
+ background-color: #81a844;
+ color: #FFF;
+ padding: 30px 30px 20px;
+ }
+
+ #description a {
+ color: #fff;
+ }
+
+ #content {
+ padding: 10px 30px 30px;
+ background: #fff;
+ }
+
+ a:hover {
+ border-bottom: 1px solid;
+ }
+
+ th, td {
+ font-size: 12px;
+ }
+
+ th {
+ text-align: left;
+ border-bottom: 1px solid #ccc;
+ }
+
+ th, td {
+ padding: 10px 15px;
+ }
+
+ .odd {
+ background-color: #E7F1D4;
+ }
+
+ #footer {
+ margin: 20px 30px;
+ font-size: 12px;
+ color: #999;
+ }
+
+ #footer a {
+ color: inherit;
+ }
+
+ #description a, #footer a {
+ border-bottom: 1px solid;
+ }
+
+ #description a:hover, #footer a:hover {
+ border-bottom: none;
+ }
+
+ img.thumbnail {
+ max-height: 100px;
+ max-width: 100px;
+ }
+CSS;
+ }
+
+}
diff --git a/plugins/jetpack/modules/sitemaps/sitemaps.php b/plugins/jetpack/modules/sitemaps/sitemaps.php
new file mode 100644
index 00000000..7078fb8a
--- /dev/null
+++ b/plugins/jetpack/modules/sitemaps/sitemaps.php
@@ -0,0 +1,578 @@
+<?php
+/**
+ * Generate sitemap files in base XML as well as some namespace extensions.
+ *
+ * This module generates two different base sitemaps.
+ *
+ * 1. sitemap.xml
+ * The basic sitemap is updated regularly by wp-cron. It is stored in the
+ * database and retrieved when requested. This sitemap aims to include canonical
+ * URLs for all published content and abide by the sitemap spec. This is the root
+ * of a tree of sitemap and sitemap index xml files, depending on the number of URLs.
+ *
+ * By default the sitemap contains published posts of type 'post' and 'page', as
+ * well as the home url. To include other post types use the 'jetpack_sitemap_post_types'
+ * filter.
+ *
+ * @link http://sitemaps.org/protocol.php Base sitemaps protocol.
+ * @link https://support.google.com/webmasters/answer/178636 Image sitemap extension.
+ * @link https://developers.google.com/webmasters/videosearch/sitemaps Video sitemap extension.
+ *
+ * 2. news-sitemap.xml
+ * The news sitemap is generated on the fly when requested. It does not aim for
+ * completeness, instead including at most 1000 of the most recent published posts
+ * from the previous 2 days, per the news-sitemap spec.
+ *
+ * @link http://www.google.com/support/webmasters/bin/answer.py?answer=74288 News sitemap extension.
+ *
+ * @package Jetpack
+ * @since 3.9.0
+ * @since 4.8.0 Remove 1000 post limit.
+ * @author Automattic
+ */
+
+/* Include all of the sitemap subclasses. */
+require_once dirname( __FILE__ ) . '/sitemap-constants.php';
+require_once dirname( __FILE__ ) . '/sitemap-buffer.php';
+require_once dirname( __FILE__ ) . '/sitemap-stylist.php';
+require_once dirname( __FILE__ ) . '/sitemap-librarian.php';
+require_once dirname( __FILE__ ) . '/sitemap-finder.php';
+require_once dirname( __FILE__ ) . '/sitemap-builder.php';
+
+if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+ require_once dirname( __FILE__ ) . '/sitemap-logger.php';
+}
+
+/**
+ * Governs the generation, storage, and serving of sitemaps.
+ *
+ * @since 4.8.0
+ */
+class Jetpack_Sitemap_Manager {
+
+ /**
+ * Librarian object for storing and retrieving sitemap data.
+ *
+ * @see Jetpack_Sitemap_Librarian
+ * @since 4.8.0
+ * @var Jetpack_Sitemap_Librarian $librarian Librarian object for storing and retrieving sitemap data.
+ */
+ private $librarian;
+
+ /**
+ * Logger object for reporting debug messages.
+ *
+ * @see Jetpack_Sitemap_Logger
+ * @since 4.8.0
+ * @var Jetpack_Sitemap_Logger $logger Logger object for reporting debug messages.
+ */
+ private $logger;
+
+ /**
+ * Finder object for handling sitemap URIs.
+ *
+ * @see Jetpack_Sitemap_Finder
+ * @since 4.8.0
+ * @var Jetpack_Sitemap_Finder $finder Finder object for handling with sitemap URIs.
+ */
+ private $finder;
+
+ /**
+ * Construct a new Jetpack_Sitemap_Manager.
+ *
+ * @access public
+ * @since 4.8.0
+ */
+ public function __construct() {
+ $this->librarian = new Jetpack_Sitemap_Librarian();
+ $this->finder = new Jetpack_Sitemap_Finder();
+
+ if ( defined( 'WP_DEBUG' ) && ( true === WP_DEBUG ) ) {
+ $this->logger = new Jetpack_Sitemap_Logger();
+ }
+
+ // Add callback for sitemap URL handler.
+ add_action(
+ 'init',
+ array( $this, 'callback_action_catch_sitemap_urls' ),
+ defined( 'IS_WPCOM' ) && IS_WPCOM ? 100 : 10
+ );
+
+ // Add generator to wp_cron task list.
+ $this->schedule_sitemap_generation();
+
+ // Add sitemap to robots.txt.
+ add_action(
+ 'do_robotstxt',
+ array( $this, 'callback_action_do_robotstxt' ),
+ 20
+ );
+
+ // The news sitemap is cached; here we add a callback to
+ // flush the cached news sitemap when a post is published.
+ add_action(
+ 'publish_post',
+ array( $this, 'callback_action_flush_news_sitemap_cache' ),
+ 10
+ );
+
+ // In case we need to purge all sitemaps, we do this.
+ add_action(
+ 'jetpack_sitemaps_purge_data',
+ array( $this, 'callback_action_purge_data' )
+ );
+
+ /*
+ * Module parameters are stored as options in the database.
+ * This allows us to avoid having to process all of init
+ * before serving the sitemap data. The following actions
+ * process and store these filters.
+ */
+
+ // Process filters and store location string for sitemap.
+ add_action(
+ 'init',
+ array( $this, 'callback_action_filter_sitemap_location' ),
+ 999
+ );
+ }
+
+ /**
+ * Echo a raw string of given content-type.
+ *
+ * @access private
+ * @since 4.8.0
+ *
+ * @param string $the_content_type The content type to be served.
+ * @param string $the_content The string to be echoed.
+ */
+ private function serve_raw_and_die( $the_content_type, $the_content ) {
+ header( 'Content-Type: ' . $the_content_type . '; charset=UTF-8' );
+
+ global $wp_query;
+ $wp_query->is_feed = true;
+ set_query_var( 'feed', 'sitemap' );
+
+ if ( '' === $the_content ) {
+ $error = __( 'No sitemap found. Please try again later.', 'jetpack' );
+ if ( current_user_can( 'manage_options' ) ) {
+ $next = human_time_diff( wp_next_scheduled( 'jp_sitemap_cron_hook' ) );
+ /* translators: %s is a human_time_diff until next sitemap generation. */
+ $error = sprintf( __( 'No sitemap found. The system will try to build it again in %s.', 'jetpack' ), $next );
+ }
+
+ wp_die(
+ esc_html( $error ),
+ esc_html__( 'Sitemaps', 'jetpack' ),
+ array(
+ 'response' => 404,
+ )
+ );
+ }
+
+ echo $the_content;
+
+ die();
+ }
+
+ /**
+ * Callback to intercept sitemap url requests and serve sitemap files.
+ *
+ * @access public
+ * @since 4.8.0
+ */
+ public function callback_action_catch_sitemap_urls() {
+ // Regular expressions for sitemap URL routing.
+ $regex = array(
+ 'master' => '/^sitemap\.xml$/',
+ 'sitemap' => '/^sitemap-[1-9][0-9]*\.xml$/',
+ 'index' => '/^sitemap-index-[1-9][0-9]*\.xml$/',
+ 'sitemap-style' => '/^sitemap\.xsl$/',
+ 'index-style' => '/^sitemap-index\.xsl$/',
+ 'image' => '/^image-sitemap-[1-9][0-9]*\.xml$/',
+ 'image-index' => '/^image-sitemap-index-[1-9][0-9]*\.xml$/',
+ 'image-style' => '/^image-sitemap\.xsl$/',
+ 'video' => '/^video-sitemap-[1-9][0-9]*\.xml$/',
+ 'video-index' => '/^video-sitemap-index-[1-9][0-9]*\.xml$/',
+ 'video-style' => '/^video-sitemap\.xsl$/',
+ 'news' => '/^news-sitemap\.xml$/',
+ 'news-style' => '/^news-sitemap\.xsl$/',
+ );
+
+ // The raw path(+query) of the requested URI.
+ if ( isset( $_SERVER['REQUEST_URI'] ) ) { // WPCS: Input var okay.
+ $raw_uri = sanitize_text_field(
+ wp_unslash( $_SERVER['REQUEST_URI'] ) // WPCS: Input var okay.
+ );
+ } else {
+ $raw_uri = '';
+ }
+
+ $request = $this->finder->recognize_sitemap_uri( $raw_uri );
+
+ if ( isset( $request['sitemap_name'] ) ) {
+
+ /**
+ * Filter the content type used to serve the sitemap XML files.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param string $xml_content_type By default, it's 'text/xml'.
+ */
+ $xml_content_type = apply_filters( 'jetpack_sitemap_content_type', 'text/xml' );
+
+ // Catch master sitemap xml.
+ if ( preg_match( $regex['master'], $request['sitemap_name'] ) ) {
+ $sitemap_content = $this->librarian->get_sitemap_text(
+ jp_sitemap_filename( JP_MASTER_SITEMAP_TYPE, 0 ),
+ JP_MASTER_SITEMAP_TYPE
+ );
+
+ // if there is no master sitemap yet, let's just return an empty sitemap with a short TTL instead of a 404
+ if ( empty( $sitemap_content ) ) {
+ $builder = new Jetpack_Sitemap_Builder();
+ $sitemap_content = $builder->empty_sitemap_xml();
+ }
+
+ $this->serve_raw_and_die(
+ $xml_content_type,
+ $sitemap_content
+ );
+ }
+
+ // Catch sitemap xml.
+ if ( preg_match( $regex['sitemap'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ $xml_content_type,
+ $this->librarian->get_sitemap_text(
+ $request['sitemap_name'],
+ JP_PAGE_SITEMAP_TYPE
+ )
+ );
+ }
+
+ // Catch sitemap index xml.
+ if ( preg_match( $regex['index'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ $xml_content_type,
+ $this->librarian->get_sitemap_text(
+ $request['sitemap_name'],
+ JP_PAGE_SITEMAP_INDEX_TYPE
+ )
+ );
+ }
+
+ // Catch sitemap xsl.
+ if ( preg_match( $regex['sitemap-style'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ 'application/xml',
+ Jetpack_Sitemap_Stylist::sitemap_xsl()
+ );
+ }
+
+ // Catch sitemap index xsl.
+ if ( preg_match( $regex['index-style'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ 'application/xml',
+ Jetpack_Sitemap_Stylist::sitemap_index_xsl()
+ );
+ }
+
+ // Catch image sitemap xml.
+ if ( preg_match( $regex['image'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ $xml_content_type,
+ $this->librarian->get_sitemap_text(
+ $request['sitemap_name'],
+ JP_IMAGE_SITEMAP_TYPE
+ )
+ );
+ }
+
+ // Catch image sitemap index xml.
+ if ( preg_match( $regex['image-index'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ $xml_content_type,
+ $this->librarian->get_sitemap_text(
+ $request['sitemap_name'],
+ JP_IMAGE_SITEMAP_INDEX_TYPE
+ )
+ );
+ }
+
+ // Catch image sitemap xsl.
+ if ( preg_match( $regex['image-style'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ 'application/xml',
+ Jetpack_Sitemap_Stylist::image_sitemap_xsl()
+ );
+ }
+
+ // Catch video sitemap xml.
+ if ( preg_match( $regex['video'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ $xml_content_type,
+ $this->librarian->get_sitemap_text(
+ $request['sitemap_name'],
+ JP_VIDEO_SITEMAP_TYPE
+ )
+ );
+ }
+
+ // Catch video sitemap index xml.
+ if ( preg_match( $regex['video-index'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ $xml_content_type,
+ $this->librarian->get_sitemap_text(
+ $request['sitemap_name'],
+ JP_VIDEO_SITEMAP_INDEX_TYPE
+ )
+ );
+ }
+
+ // Catch video sitemap xsl.
+ if ( preg_match( $regex['video-style'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ 'application/xml',
+ Jetpack_Sitemap_Stylist::video_sitemap_xsl()
+ );
+ }
+
+ // Catch news sitemap xml.
+ if ( preg_match( $regex['news'], $request['sitemap_name'] ) ) {
+ $sitemap_builder = new Jetpack_Sitemap_Builder();
+ $this->serve_raw_and_die(
+ $xml_content_type,
+ $sitemap_builder->news_sitemap_xml()
+ );
+ }
+
+ // Catch news sitemap xsl.
+ if ( preg_match( $regex['news-style'], $request['sitemap_name'] ) ) {
+ $this->serve_raw_and_die(
+ 'application/xml',
+ Jetpack_Sitemap_Stylist::news_sitemap_xsl()
+ );
+ }
+ }
+ }
+
+ /**
+ * Callback for adding sitemap-interval to the list of schedules.
+ *
+ * @access public
+ * @since 4.8.0
+ *
+ * @param array $schedules The array of WP_Cron schedules.
+ *
+ * @return array The updated array of WP_Cron schedules.
+ */
+ public function callback_add_sitemap_schedule( $schedules ) {
+ $schedules['sitemap-interval'] = array(
+ 'interval' => JP_SITEMAP_INTERVAL,
+ 'display' => __( 'Sitemap Interval', 'jetpack' ),
+ );
+ return $schedules;
+ }
+
+ /**
+ * Callback handler for sitemap cron hook
+ *
+ * @access public
+ */
+ public function callback_sitemap_cron_hook() {
+ $sitemap_builder = new Jetpack_Sitemap_Builder();
+ $sitemap_builder->update_sitemap();
+ }
+
+ /**
+ * Add actions to schedule sitemap generation.
+ * Should only be called once, in the constructor.
+ *
+ * @access private
+ * @since 4.8.0
+ */
+ private function schedule_sitemap_generation() {
+ // Add cron schedule.
+ add_filter( 'cron_schedules', array( $this, 'callback_add_sitemap_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
+
+ add_action(
+ 'jp_sitemap_cron_hook',
+ array( $this, 'callback_sitemap_cron_hook' )
+ );
+
+ if ( ! wp_next_scheduled( 'jp_sitemap_cron_hook' ) ) {
+ /**
+ * Filter the delay in seconds until sitemap generation cron job is started.
+ *
+ * This filter allows a site operator or hosting provider to potentialy spread out sitemap generation for a
+ * lot of sites over time. By default, it will be randomly done over 15 minutes.
+ *
+ * @module sitemaps
+ * @since 6.6.1
+ *
+ * @param int $delay Time to delay in seconds.
+ */
+ $delay = apply_filters( 'jetpack_sitemap_generation_delay', MINUTE_IN_SECONDS * wp_rand( 1, 15 ) ); // Randomly space it out to start within next fifteen minutes.
+ wp_schedule_event(
+ time() + $delay,
+ 'sitemap-interval',
+ 'jp_sitemap_cron_hook'
+ );
+ }
+ }
+
+ /**
+ * Callback to add sitemap to robots.txt.
+ *
+ * @access public
+ * @since 4.8.0
+ */
+ public function callback_action_do_robotstxt() {
+
+ /**
+ * Filter whether to make the default sitemap discoverable to robots or not. Default true.
+ *
+ * @module sitemaps
+ * @since 3.9.0
+ *
+ * @param bool $discover_sitemap Make default sitemap discoverable to robots.
+ */
+ $discover_sitemap = apply_filters( 'jetpack_sitemap_generate', true );
+
+ if ( true === $discover_sitemap ) {
+ $sitemap_url = $this->finder->construct_sitemap_url( 'sitemap.xml' );
+ echo 'Sitemap: ' . esc_url( $sitemap_url ) . "\n";
+ }
+
+ /**
+ * Filter whether to make the news sitemap discoverable to robots or not. Default true.
+ *
+ * @module sitemaps
+ * @since 3.9.0
+ *
+ * @param bool $discover_news_sitemap Make default news sitemap discoverable to robots.
+ */
+ $discover_news_sitemap = apply_filters( 'jetpack_news_sitemap_generate', true );
+
+ if ( true === $discover_news_sitemap ) {
+ $news_sitemap_url = $this->finder->construct_sitemap_url( 'news-sitemap.xml' );
+ echo 'Sitemap: ' . esc_url( $news_sitemap_url ) . "\n";
+ }
+ }
+
+ /**
+ * Callback to delete the news sitemap cache.
+ *
+ * @access public
+ * @since 4.8.0
+ */
+ public function callback_action_flush_news_sitemap_cache() {
+ delete_transient( 'jetpack_news_sitemap_xml' );
+ }
+
+ /**
+ * Callback for resetting stored sitemap data.
+ *
+ * @access public
+ * @since 5.3.0
+ * @since 6.7.0 Schedules a regeneration.
+ */
+ public function callback_action_purge_data() {
+ $this->callback_action_flush_news_sitemap_cache();
+ $this->librarian->delete_all_stored_sitemap_data();
+ /** This filter is documented in modules/sitemaps/sitemaps.php */
+ $delay = apply_filters( 'jetpack_sitemap_generation_delay', MINUTE_IN_SECONDS * wp_rand( 1, 15 ) ); // Randomly space it out to start within next fifteen minutes.
+ wp_schedule_single_event( time() + $delay, 'jp_sitemap_cron_hook' );
+ }
+
+ /**
+ * Callback to set the sitemap location.
+ *
+ * @access public
+ * @since 4.8.0
+ */
+ public function callback_action_filter_sitemap_location() {
+ update_option(
+ 'jetpack_sitemap_location',
+ /**
+ * Additional path for sitemap URIs. Default value is empty.
+ *
+ * This string is any additional path fragment you want included between
+ * the home URL and the sitemap filenames. Exactly how this fragment is
+ * interpreted depends on your permalink settings. For example:
+ *
+ * Pretty permalinks:
+ * home_url() . jetpack_sitemap_location . '/sitemap.xml'
+ *
+ * Plain ("ugly") permalinks:
+ * home_url() . jetpack_sitemap_location . '/?jetpack-sitemap=sitemap.xml'
+ *
+ * PATHINFO permalinks:
+ * home_url() . '/index.php' . jetpack_sitemap_location . '/sitemap.xml'
+ *
+ * where 'sitemap.xml' is the name of a specific sitemap file.
+ * The value of this filter must be a valid path fragment per RFC 3986;
+ * in particular it must either be empty or begin with a '/'.
+ * Also take care that any restrictions on sitemap location imposed by
+ * the sitemap protocol are satisfied.
+ *
+ * The result of this filter is stored in an option, 'jetpack_sitemap_location';
+ * that option is what gets read when the sitemap location is needed.
+ * This way we don't have to wait for init to finish before building sitemaps.
+ *
+ * @link https://tools.ietf.org/html/rfc3986#section-3.3 RFC 3986
+ * @link http://www.sitemaps.org/ The sitemap protocol
+ *
+ * @since 4.8.0
+ */
+ apply_filters(
+ 'jetpack_sitemap_location',
+ ''
+ )
+ );
+ }
+
+} // End Jetpack_Sitemap_Manager class.
+
+new Jetpack_Sitemap_Manager();
+
+/**
+ * Absolute URL of the current blog's sitemap.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ * @since 4.8.1 Code uses method found in Jetpack_Sitemap_Finder::construct_sitemap_url in 4.8.0.
+ * It has been moved here to avoid fatal errors with other plugins that were expecting to find this function.
+ *
+ * @param string $filename Sitemap file name. Defaults to 'sitemap.xml', the initial sitemaps page.
+ *
+ * @return string Sitemap URL.
+ */
+function jetpack_sitemap_uri( $filename = 'sitemap.xml' ) {
+ global $wp_rewrite;
+
+ $location = Jetpack_Options::get_option_and_ensure_autoload( 'jetpack_sitemap_location', '' );
+
+ if ( $wp_rewrite->using_index_permalinks() ) {
+ $sitemap_url = home_url( '/index.php' . $location . '/' . $filename );
+ } elseif ( $wp_rewrite->using_permalinks() ) {
+ $sitemap_url = home_url( $location . '/' . $filename );
+ } else {
+ $sitemap_url = home_url( $location . '/?jetpack-sitemap=' . $filename );
+ }
+
+ /**
+ * Filter sitemap URL relative to home URL.
+ *
+ * @module sitemaps
+ *
+ * @since 3.9.0
+ *
+ * @param string $sitemap_url Sitemap URL.
+ */
+ return apply_filters( 'jetpack_sitemap_location', $sitemap_url );
+}
diff --git a/plugins/jetpack/modules/social-links.php b/plugins/jetpack/modules/social-links.php
new file mode 100644
index 00000000..88c13090
--- /dev/null
+++ b/plugins/jetpack/modules/social-links.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer needed.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/sso.php b/plugins/jetpack/modules/sso.php
new file mode 100644
index 00000000..34b9609c
--- /dev/null
+++ b/plugins/jetpack/modules/sso.php
@@ -0,0 +1,1138 @@
+<?php
+require_once( JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-helpers.php' );
+require_once( JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-notices.php' );
+
+/**
+ * Module Name: Secure Sign On
+ * Module Description: Allow users to log into this site using WordPress.com accounts
+ * Jumpstart Description: Lets you log in to all your Jetpack-enabled sites with one click using your WordPress.com account.
+ * Sort Order: 30
+ * Recommendation Order: 5
+ * First Introduced: 2.6
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Developers
+ * Feature: Security, Jumpstart
+ * Additional Search Queries: sso, single sign on, login, log in
+ */
+
+class Jetpack_SSO {
+ static $instance = null;
+
+ private function __construct() {
+
+ self::$instance = $this;
+
+ add_action( 'admin_init', array( $this, 'maybe_authorize_user_after_sso' ), 1 );
+ add_action( 'admin_init', array( $this, 'register_settings' ) );
+ add_action( 'login_init', array( $this, 'login_init' ) );
+ add_action( 'delete_user', array( $this, 'delete_connection_for_user' ) );
+ add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
+ add_action( 'init', array( $this, 'maybe_logout_user' ), 5 );
+ add_action( 'jetpack_modules_loaded', array( $this, 'module_configure_button' ) );
+ add_action( 'login_form_logout', array( $this, 'store_wpcom_profile_cookies_on_logout' ) );
+ add_action( 'jetpack_unlinked_user', array( $this, 'delete_connection_for_user') );
+ add_action( 'wp_login', array( 'Jetpack_SSO', 'clear_cookies_after_login' ) );
+ add_action( 'jetpack_jitm_received_envelopes', array( $this, 'inject_sso_jitm' ) );
+
+ // Adding this action so that on login_init, the action won't be sanitized out of the $action global.
+ add_action( 'login_form_jetpack-sso', '__return_true' );
+ }
+
+ /**
+ * Returns the single instance of the Jetpack_SSO object
+ *
+ * @since 2.8
+ * @return Jetpack_SSO
+ **/
+ public static function get_instance() {
+ if ( ! is_null( self::$instance ) ) {
+ return self::$instance;
+ }
+
+ return self::$instance = new Jetpack_SSO;
+ }
+
+ /**
+ * Add configure button and functionality to the module card on the Jetpack screen
+ **/
+ public static function module_configure_button() {
+ Jetpack::enable_module_configurable( __FILE__ );
+ }
+
+ /**
+ * If jetpack_force_logout == 1 in current user meta the user will be forced
+ * to logout and reauthenticate with the site.
+ **/
+ public function maybe_logout_user() {
+ global $current_user;
+
+ if ( 1 == $current_user->jetpack_force_logout ) {
+ delete_user_meta( $current_user->ID, 'jetpack_force_logout' );
+ self::delete_connection_for_user( $current_user->ID );
+ wp_logout();
+ wp_safe_redirect( wp_login_url() );
+ exit;
+ }
+ }
+
+ /**
+ * Adds additional methods the WordPress xmlrpc API for handling SSO specific features
+ *
+ * @param array $methods
+ * @return array
+ **/
+ public function xmlrpc_methods( $methods ) {
+ $methods['jetpack.userDisconnect'] = array( $this, 'xmlrpc_user_disconnect' );
+ return $methods;
+ }
+
+ /**
+ * Marks a user's profile for disconnect from WordPress.com and forces a logout
+ * the next time the user visits the site.
+ **/
+ public function xmlrpc_user_disconnect( $user_id ) {
+ $user_query = new WP_User_Query(
+ array(
+ 'meta_key' => 'wpcom_user_id',
+ 'meta_value' => $user_id,
+ )
+ );
+ $user = $user_query->get_results();
+ $user = $user[0];
+
+ if ( $user instanceof WP_User ) {
+ $user = wp_set_current_user( $user->ID );
+ update_user_meta( $user->ID, 'jetpack_force_logout', '1' );
+ self::delete_connection_for_user( $user->ID );
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Enqueues scripts and styles necessary for SSO login.
+ */
+ public function login_enqueue_scripts() {
+ global $action;
+
+ if ( ! Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
+ return;
+ }
+
+ if ( is_rtl() ) {
+ wp_enqueue_style( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login-rtl.css', JETPACK__PLUGIN_FILE ), array( 'login', 'genericons' ), JETPACK__VERSION );
+ } else {
+ wp_enqueue_style( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login.css', JETPACK__PLUGIN_FILE ), array( 'login', 'genericons' ), JETPACK__VERSION );
+ }
+
+ wp_enqueue_script( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), JETPACK__VERSION );
+ }
+
+ /**
+ * Adds Jetpack SSO classes to login body
+ *
+ * @param array $classes Array of classes to add to body tag
+ * @return array Array of classes to add to body tag
+ */
+ public function login_body_class( $classes ) {
+ global $action;
+
+ if ( ! Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
+ return $classes;
+ }
+
+ // Always add the jetpack-sso class so that we can add SSO specific styling even when the SSO form isn't being displayed.
+ $classes[] = 'jetpack-sso';
+
+ if ( ! Jetpack::is_staging_site() ) {
+ /**
+ * Should we show the SSO login form?
+ *
+ * $_GET['jetpack-sso-default-form'] is used to provide a fallback in case JavaScript is not enabled.
+ *
+ * The default_to_sso_login() method allows us to dynamically decide whether we show the SSO login form or not.
+ * The SSO module uses the method to display the default login form if we can not find a user to log in via SSO.
+ * But, the method could be filtered by a site admin to always show the default login form if that is preferred.
+ */
+ if ( empty( $_GET['jetpack-sso-show-default-form'] ) && Jetpack_SSO_Helpers::show_sso_login() ) {
+ $classes[] = 'jetpack-sso-form-display';
+ }
+ }
+
+ return $classes;
+ }
+
+ public function print_inline_admin_css() {
+ ?>
+ <style>
+ .jetpack-sso .message {
+ margin-top: 20px;
+ }
+
+ .jetpack-sso #login .message:first-child,
+ .jetpack-sso #login h1 + .message {
+ margin-top: 0;
+ }
+ </style>
+ <?php
+ }
+
+ /**
+ * Adds settings fields to Settings > General > Secure Sign On that allows users to
+ * turn off the login form on wp-login.php
+ *
+ * @since 2.7
+ **/
+ public function register_settings() {
+
+ add_settings_section(
+ 'jetpack_sso_settings',
+ __( 'Secure Sign On' , 'jetpack' ),
+ '__return_false',
+ 'jetpack-sso'
+ );
+
+ /*
+ * Settings > General > Secure Sign On
+ * Require two step authentication
+ */
+ register_setting(
+ 'jetpack-sso',
+ 'jetpack_sso_require_two_step',
+ array( $this, 'validate_jetpack_sso_require_two_step' )
+ );
+
+ add_settings_field(
+ 'jetpack_sso_require_two_step',
+ '', // __( 'Require Two-Step Authentication' , 'jetpack' ),
+ array( $this, 'render_require_two_step' ),
+ 'jetpack-sso',
+ 'jetpack_sso_settings'
+ );
+
+ /*
+ * Settings > General > Secure Sign On
+ */
+ register_setting(
+ 'jetpack-sso',
+ 'jetpack_sso_match_by_email',
+ array( $this, 'validate_jetpack_sso_match_by_email' )
+ );
+
+ add_settings_field(
+ 'jetpack_sso_match_by_email',
+ '', // __( 'Match by Email' , 'jetpack' ),
+ array( $this, 'render_match_by_email' ),
+ 'jetpack-sso',
+ 'jetpack_sso_settings'
+ );
+ }
+
+ /**
+ * Builds the display for the checkbox allowing user to require two step
+ * auth be enabled on WordPress.com accounts before login. Displays in Settings > General
+ *
+ * @since 2.7
+ **/
+ public function render_require_two_step() {
+ ?>
+ <label>
+ <input
+ type="checkbox"
+ name="jetpack_sso_require_two_step"
+ <?php checked( Jetpack_SSO_Helpers::is_two_step_required() ); ?>
+ <?php disabled( Jetpack_SSO_Helpers::is_require_two_step_checkbox_disabled() ); ?>
+ >
+ <?php esc_html_e( 'Require Two-Step Authentication' , 'jetpack' ); ?>
+ </label>
+ <?php
+ }
+
+ /**
+ * Validate the require two step checkbox in Settings > General
+ *
+ * @since 2.7
+ * @return boolean
+ **/
+ public function validate_jetpack_sso_require_two_step( $input ) {
+ return ( ! empty( $input ) ) ? 1 : 0;
+ }
+
+ /**
+ * Builds the display for the checkbox allowing the user to allow matching logins by email
+ * Displays in Settings > General
+ *
+ * @since 2.9
+ **/
+ public function render_match_by_email() {
+ ?>
+ <label>
+ <input
+ type="checkbox"
+ name="jetpack_sso_match_by_email"
+ <?php checked( Jetpack_SSO_Helpers::match_by_email() ); ?>
+ <?php disabled( Jetpack_SSO_Helpers::is_match_by_email_checkbox_disabled() ); ?>
+ >
+ <?php esc_html_e( 'Match by Email', 'jetpack' ); ?>
+ </label>
+ <?php
+ }
+
+ /**
+ * Validate the match by email check in Settings > General
+ *
+ * @since 2.9
+ * @return boolean
+ **/
+ public function validate_jetpack_sso_match_by_email( $input ) {
+ return ( ! empty( $input ) ) ? 1 : 0;
+ }
+
+ /**
+ * Checks to determine if the user wants to login on wp-login
+ *
+ * This function mostly exists to cover the exceptions to login
+ * that may exist as other parameters to $_GET[action] as $_GET[action]
+ * does not have to exist. By default WordPress assumes login if an action
+ * is not set, however this may not be true, as in the case of logout
+ * where $_GET[loggedout] is instead set
+ *
+ * @return boolean
+ **/
+ private function wants_to_login() {
+ $wants_to_login = false;
+
+ // Cover default WordPress behavior
+ $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : 'login';
+
+ // And now the exceptions
+ $action = isset( $_GET['loggedout'] ) ? 'loggedout' : $action;
+
+ if ( Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
+ $wants_to_login = true;
+ }
+
+ return $wants_to_login;
+ }
+
+ function login_init() {
+ global $action;
+
+ if ( Jetpack_SSO_Helpers::should_hide_login_form() ) {
+ /**
+ * Since the default authenticate filters fire at priority 20 for checking username and password,
+ * let's fire at priority 30. wp_authenticate_spam_check is fired at priority 99, but since we return a
+ * WP_Error in disable_default_login_form, then we won't trigger spam processing logic.
+ */
+ add_filter( 'authenticate', array( 'Jetpack_SSO_Notices', 'disable_default_login_form' ), 30 );
+
+ /**
+ * Filter the display of the disclaimer message appearing when default WordPress login form is disabled.
+ *
+ * @module sso
+ *
+ * @since 2.8.0
+ *
+ * @param bool true Should the disclaimer be displayed. Default to true.
+ */
+ $display_sso_disclaimer = apply_filters( 'jetpack_sso_display_disclaimer', true );
+ if ( $display_sso_disclaimer ) {
+ add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'msg_login_by_jetpack' ) );
+ }
+ }
+
+ if ( 'jetpack-sso' === $action ) {
+ if ( isset( $_GET['result'], $_GET['user_id'], $_GET['sso_nonce'] ) && 'success' == $_GET['result'] ) {
+ $this->handle_login();
+ $this->display_sso_login_form();
+ } else {
+ if ( Jetpack::is_staging_site() ) {
+ add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'sso_not_allowed_in_staging' ) );
+ } else {
+ // Is it wiser to just use wp_redirect than do this runaround to wp_safe_redirect?
+ add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
+ $reauth = ! empty( $_GET['force_reauth'] );
+ $sso_url = $this->get_sso_url_or_die( $reauth );
+
+ // Is this our first SSO Login. Set an option.
+ if ( ! Jetpack_Options::get_option( 'sso_first_login' ) ) {
+ Jetpack_options::update_option( 'sso_first_login', true );
+ }
+
+ JetpackTracking::record_user_event( 'sso_login_redirect_success' );
+ wp_safe_redirect( $sso_url );
+ exit;
+ }
+ }
+ } else if ( Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
+
+ // Save cookies so we can handle redirects after SSO
+ $this->save_cookies();
+
+ /**
+ * Check to see if the site admin wants to automagically forward the user
+ * to the WordPress.com login page AND that the request to wp-login.php
+ * is not something other than login (Like logout!)
+ */
+ if ( Jetpack_SSO_Helpers::bypass_login_forward_wpcom() && $this->wants_to_login() ) {
+ add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
+ $reauth = ! empty( $_GET['force_reauth'] );
+ $sso_url = $this->get_sso_url_or_die( $reauth );
+ JetpackTracking::record_user_event( 'sso_login_redirect_bypass_success' );
+ wp_safe_redirect( $sso_url );
+ exit;
+ }
+
+ $this->display_sso_login_form();
+ }
+ }
+
+ /**
+ * Ensures that we can get a nonce from WordPress.com via XML-RPC before setting
+ * up the hooks required to display the SSO form.
+ */
+ public function display_sso_login_form() {
+ add_filter( 'login_body_class', array( $this, 'login_body_class' ) );
+ add_action( 'login_head', array( $this, 'print_inline_admin_css' ) );
+
+ if ( Jetpack::is_staging_site() ) {
+ add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'sso_not_allowed_in_staging' ) );
+ return;
+ }
+
+ $sso_nonce = self::request_initial_nonce();
+ if ( is_wp_error( $sso_nonce ) ) {
+ return;
+ }
+
+ add_action( 'login_form', array( $this, 'login_form' ) );
+ add_action( 'login_enqueue_scripts', array( $this, 'login_enqueue_scripts' ) );
+ }
+
+ /**
+ * Conditionally save the redirect_to url as a cookie.
+ *
+ * @since 4.6.0 Renamed to save_cookies from maybe_save_redirect_cookies
+ */
+ public static function save_cookies() {
+ if ( headers_sent() ) {
+ return new WP_Error( 'headers_sent', __( 'Cannot deal with cookie redirects, as headers are already sent.', 'jetpack' ) );
+ }
+
+ setcookie(
+ 'jetpack_sso_original_request',
+ esc_url_raw( set_url_scheme( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ) ),
+ time() + HOUR_IN_SECONDS,
+ COOKIEPATH,
+ COOKIE_DOMAIN,
+ is_ssl(),
+ true
+ );
+
+ if ( ! empty( $_GET['redirect_to'] ) ) {
+ // If we have something to redirect to
+ $url = esc_url_raw( $_GET['redirect_to'] );
+ setcookie( 'jetpack_sso_redirect_to', $url, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
+ } elseif ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
+ // Otherwise, if it's already set, purge it.
+ setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
+ }
+ }
+
+ /**
+ * Outputs the Jetpack SSO button and description as well as the toggle link
+ * for switching between Jetpack SSO and default login.
+ */
+ function login_form() {
+ $site_name = get_bloginfo( 'name' );
+ if ( ! $site_name ) {
+ $site_name = get_bloginfo( 'url' );
+ }
+
+ $display_name = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] )
+ ? $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ]
+ : false;
+ $gravatar = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] )
+ ? $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ]
+ : false;
+
+ ?>
+ <div id="jetpack-sso-wrap">
+ <?php if ( $display_name && $gravatar ) : ?>
+ <div id="jetpack-sso-wrap__user">
+ <img width="72" height="72" src="<?php echo esc_html( $gravatar ); ?>" />
+
+ <h2>
+ <?php
+ echo wp_kses(
+ sprintf( __( 'Log in as <span>%s</span>', 'jetpack' ), esc_html( $display_name ) ),
+ array( 'span' => true )
+ );
+ ?>
+ </h2>
+ </div>
+
+ <?php endif; ?>
+
+
+ <div id="jetpack-sso-wrap__action">
+ <?php echo $this->build_sso_button( array(), 'is_primary' ); ?>
+
+ <?php if ( $display_name && $gravatar ) : ?>
+ <a rel="nofollow" class="jetpack-sso-wrap__reauth" href="<?php echo esc_url( $this->build_sso_button_url( array( 'force_reauth' => '1' ) ) ); ?>">
+ <?php esc_html_e( 'Log in as a different WordPress.com user', 'jetpack' ); ?>
+ </a>
+ <?php else : ?>
+ <p>
+ <?php
+ echo esc_html(
+ sprintf(
+ __( 'You can now save time spent logging in by connecting your WordPress.com account to %s.', 'jetpack' ),
+ esc_html( $site_name )
+ )
+ );
+ ?>
+ </p>
+ <?php endif; ?>
+ </div>
+
+ <?php if ( ! Jetpack_SSO_Helpers::should_hide_login_form() ) : ?>
+ <div class="jetpack-sso-or">
+ <span><?php esc_html_e( 'Or', 'jetpack' ); ?></span>
+ </div>
+
+ <a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '1' ) ); ?>" class="jetpack-sso-toggle wpcom">
+ <?php
+ esc_html_e( 'Log in with username and password', 'jetpack' )
+ ?>
+ </a>
+
+ <a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '0' ) ); ?>" class="jetpack-sso-toggle default">
+ <?php
+ esc_html_e( 'Log in with WordPress.com', 'jetpack' )
+ ?>
+ </a>
+ <?php endif; ?>
+ </div>
+ <?php
+ }
+
+ /**
+ * Clear the cookies that store the profile information for the last
+ * WPCOM user to connect.
+ */
+ static function clear_wpcom_profile_cookies() {
+ if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) {
+ setcookie(
+ 'jetpack_sso_wpcom_name_' . COOKIEHASH,
+ ' ',
+ time() - YEAR_IN_SECONDS,
+ COOKIEPATH,
+ COOKIE_DOMAIN,
+ is_ssl()
+ );
+ }
+
+ if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) {
+ setcookie(
+ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
+ ' ',
+ time() - YEAR_IN_SECONDS,
+ COOKIEPATH,
+ COOKIE_DOMAIN,
+ is_ssl()
+ );
+ }
+ }
+
+ /**
+ * Clear cookies that are no longer needed once the user has logged in.
+ *
+ * @since 4.8.0
+ */
+ static function clear_cookies_after_login() {
+ self::clear_wpcom_profile_cookies();
+ if ( isset( $_COOKIE[ 'jetpack_sso_nonce' ] ) ) {
+ setcookie(
+ 'jetpack_sso_nonce',
+ ' ',
+ time() - YEAR_IN_SECONDS,
+ COOKIEPATH,
+ COOKIE_DOMAIN,
+ is_ssl()
+ );
+ }
+
+ if ( isset( $_COOKIE[ 'jetpack_sso_original_request' ] ) ) {
+ setcookie(
+ 'jetpack_sso_original_request',
+ ' ',
+ time() - YEAR_IN_SECONDS,
+ COOKIEPATH,
+ COOKIE_DOMAIN,
+ is_ssl()
+ );
+ }
+
+ if ( isset( $_COOKIE[ 'jetpack_sso_redirect_to' ] ) ) {
+ setcookie(
+ 'jetpack_sso_redirect_to',
+ ' ',
+ time() - YEAR_IN_SECONDS,
+ COOKIEPATH,
+ COOKIE_DOMAIN,
+ is_ssl()
+ );
+ }
+ }
+
+ static function delete_connection_for_user( $user_id ) {
+ if ( ! $wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true ) ) {
+ return;
+ }
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'wpcom_user_id' => $user_id,
+ ) );
+ $xml->query( 'jetpack.sso.removeUser', $wpcom_user_id );
+
+ if ( $xml->isError() ) {
+ return false;
+ }
+
+ // Clean up local data stored for SSO
+ delete_user_meta( $user_id, 'wpcom_user_id' );
+ delete_user_meta( $user_id, 'wpcom_user_data' );
+ self::clear_wpcom_profile_cookies();
+
+ return $xml->getResponse();
+ }
+
+ static function request_initial_nonce() {
+ $nonce = ! empty( $_COOKIE[ 'jetpack_sso_nonce' ] )
+ ? $_COOKIE[ 'jetpack_sso_nonce' ]
+ : false;
+
+ if ( ! $nonce ) {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id(),
+ ) );
+ $xml->query( 'jetpack.sso.requestNonce' );
+
+ if ( $xml->isError() ) {
+ return new WP_Error( $xml->getErrorCode(), $xml->getErrorMessage() );
+ }
+
+ $nonce = $xml->getResponse();
+
+ setcookie(
+ 'jetpack_sso_nonce',
+ $nonce,
+ time() + ( 10 * MINUTE_IN_SECONDS ),
+ COOKIEPATH,
+ COOKIE_DOMAIN,
+ is_ssl()
+ );
+ }
+
+ return sanitize_key( $nonce );
+ }
+
+ /**
+ * The function that actually handles the login!
+ */
+ function handle_login() {
+ $wpcom_nonce = sanitize_key( $_GET['sso_nonce'] );
+ $wpcom_user_id = (int) $_GET['user_id'];
+
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id(),
+ ) );
+ $xml->query( 'jetpack.sso.validateResult', $wpcom_nonce, $wpcom_user_id );
+
+ $user_data = $xml->isError() ? false : $xml->getResponse();
+ if ( empty( $user_data ) ) {
+ add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' );
+ add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_invalid_response_data' ) );
+ return;
+ }
+
+ $user_data = (object) $user_data;
+ $user = null;
+
+ /**
+ * Fires before Jetpack's SSO modifies the log in form.
+ *
+ * @module sso
+ *
+ * @since 2.6.0
+ *
+ * @param object $user_data WordPress.com User information.
+ */
+ do_action( 'jetpack_sso_pre_handle_login', $user_data );
+
+ if ( Jetpack_SSO_Helpers::is_two_step_required() && 0 === (int) $user_data->two_step_enabled ) {
+ $this->user_data = $user_data;
+
+ JetpackTracking::record_user_event( 'sso_login_failed', array(
+ 'error_message' => 'error_msg_enable_two_step'
+ ) );
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ do_action( 'wp_login_failed', $user_data->login );
+ add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_enable_two_step' ) );
+ return;
+ }
+
+ $user_found_with = '';
+ if ( empty( $user ) && isset( $user_data->external_user_id ) ) {
+ $user_found_with = 'external_user_id';
+ $user = get_user_by( 'id', intval( $user_data->external_user_id ) );
+ if ( $user ) {
+ update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
+ }
+ }
+
+ // If we don't have one by wpcom_user_id, try by the email?
+ if ( empty( $user ) && Jetpack_SSO_Helpers::match_by_email() ) {
+ $user_found_with = 'match_by_email';
+ $user = get_user_by( 'email', $user_data->email );
+ if ( $user ) {
+ update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
+ }
+ }
+
+ // If we've still got nothing, create the user.
+ $new_user_override_role = false;
+ if ( empty( $user ) && ( get_option( 'users_can_register' ) || ( $new_user_override_role = Jetpack_SSO_Helpers::new_user_override( $user_data ) ) ) ) {
+ /**
+ * If not matching by email we still need to verify the email does not exist
+ * or this blows up
+ *
+ * If match_by_email is true, we know the email doesn't exist, as it would have
+ * been found in the first pass. If get_user_by( 'email' ) doesn't find the
+ * user, then we know that email is unused, so it's safe to add.
+ */
+ if ( Jetpack_SSO_Helpers::match_by_email() || ! get_user_by( 'email', $user_data->email ) ) {
+
+ if ( $new_user_override_role ) {
+ $user_data->role = $new_user_override_role;
+ }
+
+ $user = Jetpack_SSO_Helpers::generate_user( $user_data );
+ if ( ! $user ) {
+ JetpackTracking::record_user_event( 'sso_login_failed', array(
+ 'error_message' => 'could_not_create_username'
+ ) );
+ add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_unable_to_create_user' ) );
+ return;
+ }
+
+ $user_found_with = $new_user_override_role
+ ? 'user_created_new_user_override'
+ : 'user_created_users_can_register';
+ } else {
+ JetpackTracking::record_user_event( 'sso_login_failed', array(
+ 'error_message' => 'error_msg_email_already_exists'
+ ) );
+
+ $this->user_data = $user_data;
+ add_action( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_email_already_exists' ) );
+ return;
+ }
+ }
+
+ /**
+ * Fires after we got login information from WordPress.com.
+ *
+ * @module sso
+ *
+ * @since 2.6.0
+ *
+ * @param array $user Local User information.
+ * @param object $user_data WordPress.com User Login information.
+ */
+ do_action( 'jetpack_sso_handle_login', $user, $user_data );
+
+ if ( $user ) {
+ // Cache the user's details, so we can present it back to them on their user screen
+ update_user_meta( $user->ID, 'wpcom_user_data', $user_data );
+
+ add_filter( 'auth_cookie_expiration', array( 'Jetpack_SSO_Helpers', 'extend_auth_cookie_expiration_for_sso' ) );
+ wp_set_auth_cookie( $user->ID, true );
+ remove_filter( 'auth_cookie_expiration', array( 'Jetpack_SSO_Helpers', 'extend_auth_cookie_expiration_for_sso' ) );
+
+ /** This filter is documented in core/src/wp-includes/user.php */
+ do_action( 'wp_login', $user->user_login, $user );
+
+ wp_set_current_user( $user->ID );
+
+ $_request_redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( $_REQUEST['redirect_to'] ) : '';
+ $redirect_to = user_can( $user, 'edit_posts' ) ? admin_url() : self::profile_page_url();
+
+ // If we have a saved redirect to request in a cookie
+ if ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
+ // Set that as the requested redirect to
+ $redirect_to = $_request_redirect_to = esc_url_raw( $_COOKIE['jetpack_sso_redirect_to'] );
+ }
+
+ $json_api_auth_environment = Jetpack_SSO_Helpers::get_json_api_auth_environment();
+
+ $is_json_api_auth = ! empty( $json_api_auth_environment );
+ $is_user_connected = Jetpack::is_user_connected( $user->ID );
+ JetpackTracking::record_user_event( 'sso_user_logged_in', array(
+ 'user_found_with' => $user_found_with,
+ 'user_connected' => (bool) $is_user_connected,
+ 'user_role' => Jetpack::translate_current_user_to_role(),
+ 'is_json_api_auth' => (bool) $is_json_api_auth,
+ ) );
+
+ if ( $is_json_api_auth ) {
+ Jetpack::init()->verify_json_api_authorization_request( $json_api_auth_environment );
+ Jetpack::init()->store_json_api_authorization_token( $user->user_login, $user );
+
+ } else if ( ! $is_user_connected ) {
+ $calypso_env = ! empty( $_GET['calypso_env'] )
+ ? sanitize_key( $_GET['calypso_env'] )
+ : '';
+
+ wp_safe_redirect(
+ add_query_arg(
+ array(
+ 'redirect_to' => $redirect_to,
+ 'request_redirect_to' => $_request_redirect_to,
+ 'calypso_env' => $calypso_env,
+ 'jetpack-sso-auth-redirect' => '1',
+ ),
+ admin_url()
+ )
+ );
+ exit;
+ }
+
+ add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
+ wp_safe_redirect(
+ /** This filter is documented in core/src/wp-login.php */
+ apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user )
+ );
+ exit;
+ }
+
+ add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' );
+
+ JetpackTracking::record_user_event( 'sso_login_failed', array(
+ 'error_message' => 'cant_find_user'
+ ) );
+
+ $this->user_data = $user_data;
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ do_action( 'wp_login_failed', $user_data->login );
+ add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'cant_find_user' ) );
+ }
+
+ static function profile_page_url() {
+ return admin_url( 'profile.php' );
+ }
+
+ /**
+ * Builds the "Login to WordPress.com" button that is displayed on the login page as well as user profile page.
+ *
+ * @param array $args An array of arguments to add to the SSO URL.
+ * @param boolean $is_primary Should the button have the `button-primary` class?
+ * @return string Returns the HTML markup for the button.
+ */
+ function build_sso_button( $args = array(), $is_primary = false ) {
+ $url = $this->build_sso_button_url( $args );
+ $classes = $is_primary
+ ? 'jetpack-sso button button-primary'
+ : 'jetpack-sso button';
+
+ return sprintf(
+ '<a rel="nofollow" href="%1$s" class="%2$s"><span>%3$s %4$s</span></a>',
+ esc_url( $url ),
+ $classes,
+ '<span class="genericon genericon-wordpress"></span>',
+ esc_html__( 'Log in with WordPress.com', 'jetpack' )
+ );
+ }
+
+ /**
+ * Builds a URL with `jetpack-sso` action and option args which is used to setup SSO.
+ *
+ * @param array $args An array of arguments to add to the SSO URL.
+ * @return string The URL used for SSO.
+ */
+ function build_sso_button_url( $args = array() ) {
+ $defaults = array(
+ 'action' => 'jetpack-sso',
+ );
+
+ $args = wp_parse_args( $args, $defaults );
+
+ if ( ! empty( $_GET['redirect_to'] ) ) {
+ $args['redirect_to'] = urlencode( esc_url_raw( $_GET['redirect_to'] ) );
+ }
+
+ return add_query_arg( $args, wp_login_url() );
+ }
+
+ /**
+ * Retrieves a WordPress.com SSO URL with appropriate query parameters or dies.
+ *
+ * @param boolean $reauth Should the user be forced to reauthenticate on WordPress.com?
+ * @param array $args Optional query parameters.
+ * @return string The WordPress.com SSO URL.
+ */
+ function get_sso_url_or_die( $reauth = false, $args = array() ) {
+ if ( empty( $reauth ) ) {
+ $sso_redirect = $this->build_sso_url( $args );
+ } else {
+ self::clear_wpcom_profile_cookies();
+ $sso_redirect = $this->build_reauth_and_sso_url( $args );
+ }
+
+ // If there was an error retrieving the SSO URL, then error.
+ if ( is_wp_error( $sso_redirect ) ) {
+ $error_message = sanitize_text_field(
+ sprintf( '%s: %s', $sso_redirect->get_error_code(), $sso_redirect->get_error_message() )
+ );
+ JetpackTracking::record_user_event( 'sso_login_redirect_failed', array(
+ 'error_message' => $error_message
+ ) );
+ wp_die( $error_message );
+ }
+
+ return $sso_redirect;
+ }
+
+ /**
+ * Build WordPress.com SSO URL with appropriate query parameters.
+ *
+ * @param array $args Optional query parameters.
+ * @return string WordPress.com SSO URL
+ */
+ function build_sso_url( $args = array() ) {
+ $sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce();
+ $defaults = array(
+ 'action' => 'jetpack-sso',
+ 'site_id' => Jetpack_Options::get_option( 'id' ),
+ 'sso_nonce' => $sso_nonce,
+ 'calypso_auth' => '1',
+ );
+
+ $args = wp_parse_args( $args, $defaults );
+
+ if ( is_wp_error( $args['sso_nonce'] ) ) {
+ return $args['sso_nonce'];
+ }
+
+ return add_query_arg( $args, 'https://wordpress.com/wp-login.php' );
+ }
+
+ /**
+ * Build WordPress.com SSO URL with appropriate query parameters,
+ * including the parameters necessary to force the user to reauthenticate
+ * on WordPress.com.
+ *
+ * @param array $args Optional query parameters.
+ * @return string WordPress.com SSO URL
+ */
+ function build_reauth_and_sso_url( $args = array() ) {
+ $sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce();
+ $redirect = $this->build_sso_url( array( 'force_auth' => '1', 'sso_nonce' => $sso_nonce ) );
+
+ if ( is_wp_error( $redirect ) ) {
+ return $redirect;
+ }
+
+ $defaults = array(
+ 'action' => 'jetpack-sso',
+ 'site_id' => Jetpack_Options::get_option( 'id' ),
+ 'sso_nonce' => $sso_nonce,
+ 'reauth' => '1',
+ 'redirect_to' => urlencode( $redirect ),
+ 'calypso_auth' => '1',
+ );
+
+ $args = wp_parse_args( $args, $defaults );
+
+ if ( is_wp_error( $args['sso_nonce'] ) ) {
+ return $args['sso_nonce'];
+ }
+
+ return add_query_arg( $args, 'https://wordpress.com/wp-login.php' );
+ }
+
+ /**
+ * Determines local user associated with a given WordPress.com user ID.
+ *
+ * @since 2.6.0
+ *
+ * @param int $wpcom_user_id User ID from WordPress.com
+ * @return object Local user object if found, null if not.
+ */
+ static function get_user_by_wpcom_id( $wpcom_user_id ) {
+ $user_query = new WP_User_Query( array(
+ 'meta_key' => 'wpcom_user_id',
+ 'meta_value' => intval( $wpcom_user_id ),
+ 'number' => 1,
+ ) );
+
+ $users = $user_query->get_results();
+ return $users ? array_shift( $users ) : null;
+ }
+
+ /**
+ * When jetpack-sso-auth-redirect query parameter is set, will redirect user to
+ * WordPress.com authorization flow.
+ *
+ * We redirect here instead of in handle_login() because Jetpack::init()->build_connect_url
+ * calls menu_page_url() which doesn't work properly until admin menus are registered.
+ */
+ function maybe_authorize_user_after_sso() {
+ if ( empty( $_GET['jetpack-sso-auth-redirect'] ) ) {
+ return;
+ }
+
+ $redirect_to = ! empty( $_GET['redirect_to'] ) ? esc_url_raw( $_GET['redirect_to'] ) : admin_url();
+ $request_redirect_to = ! empty( $_GET['request_redirect_to'] ) ? esc_url_raw( $_GET['request_redirect_to'] ) : $redirect_to;
+
+ /** This filter is documented in core/src/wp-login.php */
+ $redirect_after_auth = apply_filters( 'login_redirect', $redirect_to, $request_redirect_to, wp_get_current_user() );
+
+ /**
+ * Since we are passing this redirect to WordPress.com and therefore can not use wp_safe_redirect(),
+ * let's sanitize it here to make sure it's safe. If the redirect is not safe, then use admin_url().
+ */
+ $redirect_after_auth = wp_sanitize_redirect( $redirect_after_auth );
+ $redirect_after_auth = wp_validate_redirect( $redirect_after_auth, admin_url() );
+
+ /**
+ * Return the raw connect URL with our redirect and attribute connection to SSO.
+ */
+ $connect_url = Jetpack::init()->build_connect_url( true, $redirect_after_auth, 'sso' );
+
+ add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
+ wp_safe_redirect( $connect_url );
+ exit;
+ }
+
+ /**
+ * Cache user's display name and Gravatar so it can be displayed on the login screen. These cookies are
+ * stored when the user logs out, and then deleted when the user logs in.
+ */
+ function store_wpcom_profile_cookies_on_logout() {
+ if ( ! Jetpack::is_user_connected( get_current_user_id() ) ) {
+ return;
+ }
+
+ $user_data = $this->get_user_data( get_current_user_id() );
+ if ( ! $user_data ) {
+ return;
+ }
+
+ setcookie(
+ 'jetpack_sso_wpcom_name_' . COOKIEHASH,
+ $user_data->display_name,
+ time() + WEEK_IN_SECONDS,
+ COOKIEPATH,
+ COOKIE_DOMAIN,
+ is_ssl()
+ );
+
+ setcookie(
+ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
+ get_avatar_url(
+ $user_data->email,
+ array( 'size' => 144, 'default' => 'mystery' )
+ ),
+ time() + WEEK_IN_SECONDS,
+ COOKIEPATH,
+ COOKIE_DOMAIN,
+ is_ssl()
+ );
+ }
+
+ /**
+ * Determines if a local user is connected to WordPress.com
+ *
+ * @since 2.8
+ * @param integer $user_id - Local user id
+ * @return boolean
+ **/
+ public function is_user_connected( $user_id ) {
+ return $this->get_user_data( $user_id );
+ }
+
+ /**
+ * Retrieves a user's WordPress.com data
+ *
+ * @since 2.8
+ * @param integer $user_id - Local user id
+ * @return mixed null or stdClass
+ **/
+ public function get_user_data( $user_id ) {
+ return get_user_meta( $user_id, 'wpcom_user_data', true );
+ }
+
+ /**
+ * Mark SSO as discovered when an SSO JITM is viewed.
+ *
+ * @since 6.9.0
+ *
+ * @param array $envelopes Array of JITM messages received after API call.
+ *
+ * @return array $envelopes New array of JITM messages. May now contain only one message, about SSO.
+ */
+ public function inject_sso_jitm( $envelopes ) {
+ // Bail early if that's not the first time the user uses SSO.
+ if ( true != Jetpack_Options::get_option( 'sso_first_login' ) ) {
+ return $envelopes;
+ }
+
+ // Update our option to mark that SSO was discovered.
+ Jetpack_Options::update_option( 'sso_first_login', false );
+
+ return $this->prepare_sso_first_login_jitm();
+ }
+
+ /**
+ * Prepare JITM array for new SSO users
+ *
+ * @since 6.9.0
+ *
+ * @return array $sso_first_login_jitm array containting one object of information about our message.
+ */
+ private function prepare_sso_first_login_jitm() {
+ // Build our custom SSO JITM.
+ $discover_sso_message = array(
+ 'content' => array(
+ 'message' => esc_html__( "You've successfully signed in with WordPress.com Secure Sign On!", 'jetpack' ),
+ 'icon' => 'jetpack',
+ 'list' => array(),
+ 'description' => esc_html__( 'Interested in learning more about how Secure Sign On keeps your site safer?', 'jetpack' ),
+ 'classes' => '',
+ ),
+ 'CTA' => array(
+ 'message' => esc_html__( 'Learn More', 'jetpack' ),
+ 'hook' => '',
+ 'newWindow' => true,
+ 'primary' => true,
+ ),
+ 'template' => 'default',
+ 'ttl' => 300,
+ 'id' => 'sso_discover',
+ 'feature_class' => 'sso',
+ 'expires' => 3628800,
+ 'max_dismissal' => 1,
+ 'activate_module' => null,
+ );
+
+ return array( json_decode( json_encode( $discover_sso_message ) ) );
+ }
+}
+
+Jetpack_SSO::get_instance();
diff --git a/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php b/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php
new file mode 100644
index 00000000..307e3786
--- /dev/null
+++ b/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php
@@ -0,0 +1,325 @@
+<?php
+
+if ( ! class_exists( 'Jetpack_SSO_Helpers' ) ) :
+
+/**
+ * A collection of helper functions used in the SSO module.
+ *
+ * @since 4.1.0
+ */
+class Jetpack_SSO_Helpers {
+ /**
+ * Determine if the login form should be hidden or not
+ *
+ * @return bool
+ **/
+ static function should_hide_login_form() {
+ /**
+ * Remove the default log in form, only leave the WordPress.com log in button.
+ *
+ * @module sso
+ *
+ * @since 3.1.0
+ *
+ * @param bool get_option( 'jetpack_sso_remove_login_form', false ) Should the default log in form be removed. Default to false.
+ */
+ return (bool) apply_filters( 'jetpack_remove_login_form', get_option( 'jetpack_sso_remove_login_form', false ) );
+ }
+
+ /**
+ * Returns a boolean value for whether logging in by matching the WordPress.com user email to a
+ * Jetpack site user's email is allowed.
+ *
+ * @return bool
+ */
+ static function match_by_email() {
+ $match_by_email = ( 1 == get_option( 'jetpack_sso_match_by_email', true ) ) ? true: false;
+ $match_by_email = defined( 'WPCC_MATCH_BY_EMAIL' ) ? WPCC_MATCH_BY_EMAIL : $match_by_email;
+
+ /**
+ * Link the local account to an account on WordPress.com using the same email address.
+ *
+ * @module sso
+ *
+ * @since 2.6.0
+ *
+ * @param bool $match_by_email Should we link the local account to an account on WordPress.com using the same email address. Default to false.
+ */
+ return (bool) apply_filters( 'jetpack_sso_match_by_email', $match_by_email );
+ }
+
+ /**
+ * Returns a boolean for whether users are allowed to register on the Jetpack site with SSO,
+ * even though the site disallows normal registrations.
+ *
+ * @return bool
+ */
+ static function new_user_override( $user_data = null ) {
+ $new_user_override = defined( 'WPCC_NEW_USER_OVERRIDE' ) ? WPCC_NEW_USER_OVERRIDE : false;
+
+ /**
+ * Allow users to register on your site with a WordPress.com account, even though you disallow normal registrations.
+ * If you return a string that corresponds to a user role, the user will be given that role.
+ *
+ * @module sso
+ *
+ * @since 2.6.0
+ * @since 4.6 $user_data object is now passed to the jetpack_sso_new_user_override filter
+ *
+ * @param bool $new_user_override Allow users to register on your site with a WordPress.com account. Default to false.
+ * @param object|null $user_data An object containing the user data returned from WordPress.com.
+ */
+ $role = apply_filters( 'jetpack_sso_new_user_override', $new_user_override, $user_data );
+
+ if ( $role ) {
+ if ( is_string( $role ) && get_role( $role ) ) {
+ return $role;
+ } else {
+ return get_option( 'default_role' );
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns a boolean value for whether two-step authentication is required for SSO.
+ *
+ * @since 4.1.0
+ *
+ * @return bool
+ */
+ static function is_two_step_required() {
+ /**
+ * Is it required to have 2-step authentication enabled on WordPress.com to use SSO?
+ *
+ * @module sso
+ *
+ * @since 2.8.0
+ *
+ * @param bool get_option( 'jetpack_sso_require_two_step' ) Does SSO require 2-step authentication?
+ */
+ return (bool) apply_filters( 'jetpack_sso_require_two_step', get_option( 'jetpack_sso_require_two_step', false ) );
+ }
+
+ /**
+ * Returns a boolean for whether a user that is attempting to log in will be automatically
+ * redirected to WordPress.com to begin the SSO flow.
+ *
+ * @return bool
+ */
+ static function bypass_login_forward_wpcom() {
+ /**
+ * Redirect the site's log in form to WordPress.com's log in form.
+ *
+ * @module sso
+ *
+ * @since 3.1.0
+ *
+ * @param bool false Should the site's log in form be automatically forwarded to WordPress.com's log in form.
+ */
+ return (bool) apply_filters( 'jetpack_sso_bypass_login_forward_wpcom', false );
+ }
+
+ /**
+ * Returns a boolean for whether the SSO login form should be displayed as the default
+ * when both the default and SSO login form allowed.
+ *
+ * @since 4.1.0
+ *
+ * @return bool
+ */
+ static function show_sso_login() {
+ if ( self::should_hide_login_form() ) {
+ return true;
+ }
+
+ /**
+ * Display the SSO login form as the default when both the default and SSO login forms are enabled.
+ *
+ * @module sso
+ *
+ * @since 4.1.0
+ *
+ * @param bool true Should the SSO login form be displayed by default when the default login form is also enabled?
+ */
+ return (bool) apply_filters( 'jetpack_sso_default_to_sso_login', true );
+ }
+
+ /**
+ * Returns a boolean for whether the two step required checkbox, displayed on the Jetpack admin page, should be disabled.
+ *
+ * @since 4.1.0
+ *
+ * @return bool
+ */
+ static function is_require_two_step_checkbox_disabled() {
+ return (bool) has_filter( 'jetpack_sso_require_two_step' );
+ }
+
+ /**
+ * Returns a boolean for whether the match by email checkbox, displayed on the Jetpack admin page, should be disabled.
+ *
+ * @since 4.1.0
+ *
+ * @return bool
+ */
+ static function is_match_by_email_checkbox_disabled() {
+ return defined( 'WPCC_MATCH_BY_EMAIL' ) || has_filter( 'jetpack_sso_match_by_email' );
+ }
+
+ /**
+ * Returns an array of hosts that SSO will redirect to.
+ *
+ * Instead of accessing JETPACK__API_BASE within the method directly, we set it as the
+ * default for $api_base due to restrictions with testing constants in our tests.
+ *
+ * @since 4.3.0
+ * @since 4.6.0 Added public-api.wordpress.com as an allowed redirect
+ *
+ * @param array $hosts
+ * @param string $api_base
+ *
+ * @return array
+ */
+ static function allowed_redirect_hosts( $hosts, $api_base = JETPACK__API_BASE ) {
+ if ( empty( $hosts ) ) {
+ $hosts = array();
+ }
+
+ $hosts[] = 'wordpress.com';
+ $hosts[] = 'jetpack.wordpress.com';
+ $hosts[] = 'public-api.wordpress.com';
+
+ if ( false === strpos( $api_base, 'jetpack.wordpress.com/jetpack' ) ) {
+ $base_url_parts = parse_url( esc_url_raw( $api_base ) );
+ if ( $base_url_parts && ! empty( $base_url_parts[ 'host' ] ) ) {
+ $hosts[] = $base_url_parts[ 'host' ];
+ }
+ }
+
+ return array_unique( $hosts );
+ }
+
+ static function generate_user( $user_data ) {
+ $username = $user_data->login;
+ /**
+ * Determines how many times the SSO module can attempt to randomly generate a user.
+ *
+ * @module sso
+ *
+ * @since 4.3.2
+ *
+ * @param int 5 By default, SSO will attempt to random generate a user up to 5 times.
+ */
+ $num_tries = intval( apply_filters( 'jetpack_sso_allowed_username_generate_retries', 5 ) );
+
+ $tries = 0;
+ while ( ( $exists = username_exists( $username ) ) && $tries++ < $num_tries ) {
+ $username = $user_data->login . '_' . $user_data->ID . '_' . mt_rand();
+ }
+
+ if ( $exists ) {
+ return false;
+ }
+
+ $user = (object) array();
+ $user->user_pass = wp_generate_password( 20 );
+ $user->user_login = wp_slash( $username );
+ $user->user_email = wp_slash( $user_data->email );
+ $user->display_name = $user_data->display_name;
+ $user->first_name = $user_data->first_name;
+ $user->last_name = $user_data->last_name;
+ $user->url = $user_data->url;
+ $user->description = $user_data->description;
+
+ if ( isset( $user_data->role ) && $user_data->role ) {
+ $user->role = $user_data->role;
+ }
+
+ $created_user_id = wp_insert_user( $user );
+
+ update_user_meta( $created_user_id, 'wpcom_user_id', $user_data->ID );
+ return get_userdata( $created_user_id );
+ }
+
+ static function extend_auth_cookie_expiration_for_sso() {
+ /**
+ * Determines how long the auth cookie is valid for when a user logs in with SSO.
+ *
+ * @module sso
+ *
+ * @since 4.4.0
+ * @since 6.1.0 Fixed a typo. Filter was previously jetpack_sso_auth_cookie_expirtation.
+ *
+ * @param int YEAR_IN_SECONDS
+ */
+ return intval( apply_filters( 'jetpack_sso_auth_cookie_expiration', YEAR_IN_SECONDS ) );
+ }
+
+ /**
+ * Determines if the SSO form should be displayed for the current action.
+ *
+ * @since 4.6.0
+ *
+ * @param string $action
+ *
+ * @return bool Is SSO allowed for the current action?
+ */
+ static function display_sso_form_for_action( $action ) {
+ /**
+ * Allows plugins the ability to overwrite actions where the SSO form is allowed to be used.
+ *
+ * @module sso
+ *
+ * @since 4.6.0
+ *
+ * @param array $allowed_actions_for_sso
+ */
+ $allowed_actions_for_sso = (array) apply_filters( 'jetpack_sso_allowed_actions', array(
+ 'login',
+ 'jetpack-sso',
+ 'jetpack_json_api_authorization',
+ ) );
+ return in_array( $action, $allowed_actions_for_sso );
+ }
+
+ /**
+ * This method returns an environment array that is meant to simulate `$_REQUEST` when the initial
+ * JSON API auth request was made.
+ *
+ * @since 4.6.0
+ *
+ * @return array|bool
+ */
+ static function get_json_api_auth_environment() {
+ if ( empty( $_COOKIE['jetpack_sso_original_request'] ) ) {
+ return false;
+ }
+
+ $original_request = esc_url_raw( $_COOKIE['jetpack_sso_original_request'] );
+
+ $parsed_url = wp_parse_url( $original_request );
+ if ( empty( $parsed_url ) || empty( $parsed_url['query'] ) ) {
+ return false;
+ }
+
+ $args = array();
+ wp_parse_str( $parsed_url['query'], $args );
+
+ if ( empty( $args ) || empty( $args['action'] ) ) {
+ return false;
+ }
+
+ if ( 'jetpack_json_api_authorization' != $args['action'] ) {
+ return false;
+ }
+
+ return array_merge(
+ $args,
+ array( 'jetpack_json_api_original_query' => $original_request )
+ );
+ }
+}
+
+endif;
diff --git a/plugins/jetpack/modules/sso/class.jetpack-sso-notices.php b/plugins/jetpack/modules/sso/class.jetpack-sso-notices.php
new file mode 100644
index 00000000..138222ab
--- /dev/null
+++ b/plugins/jetpack/modules/sso/class.jetpack-sso-notices.php
@@ -0,0 +1,203 @@
+<?php
+
+if ( ! class_exists( 'Jetpack_SSO_Notices' ) ) :
+
+/**
+ * A collection of helper functions used in the SSO module.
+ *
+ * @since 4.4.0
+ */
+class Jetpack_SSO_Notices {
+ /**
+ * Error message displayed on the login form when two step is required and
+ * the user's account on WordPress.com does not have two step enabled.
+ *
+ * @since 2.7
+ * @param string $message
+ * @return string
+ **/
+ public static function error_msg_enable_two_step( $message ) {
+ $error = sprintf(
+ wp_kses(
+ __(
+ 'Two-Step Authentication is required to access this site. Please visit your <a href="%1$s" rel="noopener noreferrer" target="_blank">Security Settings</a> to configure <a href="%2$s" rel="noopener noreferrer" target="_blank">Two-step Authentication</a> for your account.',
+ 'jetpack'
+ ),
+ array( 'a' => array( 'href' => array() ) )
+ ),
+ 'https://wordpress.com/me/security/two-step',
+ 'https://support.wordpress.com/security/two-step-authentication/'
+ );
+
+ $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
+
+ return $message;
+ }
+
+ /**
+ * Error message displayed when the user tries to SSO, but match by email
+ * is off and they already have an account with their email address on
+ * this site.
+ *
+ * @param string $message
+ * @return string
+ */
+ public static function error_msg_email_already_exists( $message ) {
+ $error = sprintf(
+ wp_kses(
+ __(
+ 'You already have an account on this site. Please <a href="%1$s">sign in</a> with your username and password and then connect to WordPress.com.',
+ 'jetpack'
+ ),
+ array( 'a' => array( 'href' => array() ) )
+ ),
+ esc_url_raw( add_query_arg( 'jetpack-sso-show-default-form', '1', wp_login_url() ) )
+ );
+
+ $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
+
+ return $message;
+ }
+
+ /**
+ * Error message that is displayed when the current site is in an identity crisis and SSO can not be used.
+ *
+ * @since 4.3.2
+ *
+ * @param $message
+ *
+ * @return string
+ */
+ public static function error_msg_identity_crisis( $message ) {
+ $error = esc_html__( 'Logging in with WordPress.com is not currently available because this site is experiencing connection problems.', 'jetpack' );
+ $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
+ return $message;
+ }
+
+ /**
+ * Error message that is displayed when we are not able to verify the SSO nonce due to an XML error or
+ * failed validation. In either case, we prompt the user to try again or log in with username and password.
+ *
+ * @since 4.3.2
+ *
+ * @param $message
+ *
+ * @return string
+ */
+ public static function error_invalid_response_data( $message ) {
+ $error = esc_html__(
+ 'There was an error logging you in via WordPress.com, please try again or try logging in with your username and password.',
+ 'jetpack'
+ );
+ $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
+ return $message;
+ }
+
+ /**
+ * Error message that is displayed when we were not able to automatically create an account for a user
+ * after a user has logged in via SSO. By default, this message is triggered after trying to create an account 5 times.
+ *
+ * @since 4.3.2
+ *
+ * @param $message
+ *
+ * @return string
+ */
+ public static function error_unable_to_create_user( $message ) {
+ $error = esc_html__(
+ 'There was an error creating a user for you. Please contact the administrator of your site.',
+ 'jetpack'
+ );
+ $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
+ return $message;
+ }
+
+ /**
+ * When the default login form is hidden, this method is called on the 'authenticate' filter with a priority of 30.
+ * This method disables the ability to submit the default login form.
+ *
+ * @param $user
+ *
+ * @return WP_Error
+ */
+ public static function disable_default_login_form( $user ) {
+ if ( is_wp_error( $user ) ) {
+ return $user;
+ }
+
+ /**
+ * Since we're returning an error that will be shown as a red notice, let's remove the
+ * informational "blue" notice.
+ */
+ remove_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'msg_login_by_jetpack' ) );
+ return new WP_Error( 'jetpack_sso_required', self::get_sso_required_message() );
+ }
+
+ /**
+ * Message displayed when the site admin has disabled the default WordPress
+ * login form in Settings > General > Secure Sign On
+ *
+ * @since 2.7
+ * @param string $message
+ *
+ * @return string
+ **/
+ public static function msg_login_by_jetpack( $message ) {
+ $message .= sprintf( '<p class="message">%s</p>', self::get_sso_required_message() );
+ return $message;
+ }
+
+ public static function get_sso_required_message() {
+ $msg = esc_html__(
+ 'A WordPress.com account is required to access this site. Click the button below to sign in or create a free WordPress.com account.',
+ 'jetpack'
+ );
+
+ /**
+ * Filter the message displayed when the default WordPress login form is disabled.
+ *
+ * @module sso
+ *
+ * @since 2.8.0
+ *
+ * @param string $msg Disclaimer when default WordPress login form is disabled.
+ */
+ return apply_filters( 'jetpack_sso_disclaimer_message', $msg );
+ }
+
+ /**
+ * Message displayed when the user can not be found after approving the SSO process on WordPress.com
+ *
+ * @param string $message
+ * @return string
+ */
+ public static function cant_find_user( $message ) {
+ $error = esc_html__(
+ "We couldn't find your account. If you already have an account, make sure you have connected to WordPress.com.",
+ 'jetpack'
+ );
+ $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
+
+ return $message;
+ }
+
+ /**
+ * Error message that is displayed when the current site is in an identity crisis and SSO can not be used.
+ *
+ * @since 4.4.0
+ *
+ * @param $message
+ *
+ * @return string
+ */
+ public static function sso_not_allowed_in_staging( $message ) {
+ $error = esc_html__(
+ 'Logging in with WordPress.com is disabled for sites that are in staging mode.',
+ 'jetpack'
+ );
+ $message .= sprintf( '<p class="message">%s</p>', $error );
+ return $message;
+ }
+}
+
+endif;
diff --git a/plugins/jetpack/modules/sso/jetpack-sso-login-rtl.css b/plugins/jetpack/modules/sso/jetpack-sso-login-rtl.css
new file mode 100644
index 00000000..ea07b3ff
--- /dev/null
+++ b/plugins/jetpack/modules/sso/jetpack-sso-login-rtl.css
@@ -0,0 +1,177 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#loginform {
+ /* We set !important because sometimes static is added inline */
+ position: relative !important;
+ padding-bottom: 92px;
+}
+
+.jetpack-sso-repositioned #loginform {
+ padding-bottom: 26px;
+}
+
+#loginform #jetpack-sso-wrap,
+#loginform #jetpack-sso-wrap * {
+ box-sizing: border-box;
+}
+
+
+#jetpack-sso-wrap__action,
+#jetpack-sso-wrap__user{
+ display: none;
+}
+
+.jetpack-sso-form-display #jetpack-sso-wrap__action,
+.jetpack-sso-form-display #jetpack-sso-wrap__user {
+ display: block;
+}
+
+#jetpack-sso-wrap {
+ position: absolute;
+ bottom: 20px;
+ padding: 0 24px;
+ margin-right: -24px;
+ margin-left: -24px;
+ width: 100%;
+}
+
+.jetpack-sso-repositioned #jetpack-sso-wrap {
+ position: relative;
+ bottom: auto;
+ padding: 0;
+ margin-top: 16px;
+ margin-right: 0;
+ margin-left: 0;
+}
+
+.jetpack-sso-form-display #jetpack-sso-wrap {
+ position: relative;
+ bottom: auto;
+ padding: 0;
+ margin-top: 0;
+ margin-right: 0;
+ margin-left: 0;
+}
+
+#loginform #jetpack-sso-wrap p {
+ color: #777777;
+ margin-bottom: 16px;
+}
+
+#jetpack-sso-wrap a {
+ display: block;
+ width: 100%;
+ text-align: center;
+ text-decoration: none;
+}
+
+#jetpack-sso-wrap .jetpack-sso-toggle.wpcom {
+ display: none;
+}
+
+.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.wpcom {
+ display: block;
+}
+
+
+.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.default {
+ display: none;
+}
+
+
+.jetpack-sso-form-display #loginform > p,
+.jetpack-sso-form-display #loginform > div {
+ display: none;
+}
+
+.jetpack-sso-form-display #loginform #jetpack-sso-wrap {
+ display: block;
+}
+
+.jetpack-sso-form-display #loginform {
+ padding: 26px 24px;
+}
+
+.jetpack-sso-or {
+ margin-bottom: 16px;
+ position: relative;
+ text-align: center;
+}
+
+.jetpack-sso-or:before {
+ background: #E5E5E5;
+ content: '';
+ height: 1px;
+ position: absolute;
+ right: 0;
+ top: 50%;
+ width: 100%;
+}
+.jetpack-sso-or span {
+ background: #fff;
+ color: #777;
+ position: relative;
+ padding: 0 8px;
+ text-transform: uppercase
+}
+
+.jetpack-sso.button {
+ height: 36px;
+ line-height: 34px;
+ float: none;
+ margin-bottom: 16px;
+ position: relative;
+ width: 100%;
+}
+
+.jetpack-sso.button > span {
+ position: relative;
+ padding-right: 30px;
+}
+
+.jetpack-sso.button .genericon-wordpress {
+ position: absolute;
+ right: 0;
+ top: -3px;
+ font-size: 24px;
+}
+
+@media screen and ( max-width: 782px ) {
+ .jetpack-sso.button {
+ line-height: 22px;
+ }
+}
+
+#jetpack-sso-wrap__user img {
+ border-radius: 50%;
+ display: block;
+ margin: 0 auto 16px;
+}
+
+#jetpack-sso-wrap__user h2 {
+ font-size: 21px;
+ font-weight: 300;
+ margin-bottom: 16px;
+ text-align: center;
+}
+
+#jetpack-sso-wrap__user h2 span {
+ font-weight: bold;
+}
+
+.jetpack-sso-wrap__reauth {
+ margin-bottom: 16px;
+}
+
+.jetpack-sso-form-display #nav {
+ display: none;
+}
+
+.jetpack-sso-form-display #backtoblog {
+ margin: 24px 0 0;
+}
+
+.jetpack-sso-clear:after {
+ content: "";
+ display: table;
+ clear: both;
+}
diff --git a/plugins/jetpack/modules/sso/jetpack-sso-login-rtl.min.css b/plugins/jetpack/modules/sso/jetpack-sso-login-rtl.min.css
new file mode 100644
index 00000000..513d726f
--- /dev/null
+++ b/plugins/jetpack/modules/sso/jetpack-sso-login-rtl.min.css
@@ -0,0 +1 @@
+#loginform{position:relative!important;padding-bottom:92px}.jetpack-sso-repositioned #loginform{padding-bottom:26px}#loginform #jetpack-sso-wrap,#loginform #jetpack-sso-wrap *{box-sizing:border-box}#jetpack-sso-wrap__action,#jetpack-sso-wrap__user{display:none}.jetpack-sso-form-display #jetpack-sso-wrap__action,.jetpack-sso-form-display #jetpack-sso-wrap__user{display:block}#jetpack-sso-wrap{position:absolute;bottom:20px;padding:0 24px;margin-right:-24px;margin-left:-24px;width:100%}.jetpack-sso-repositioned #jetpack-sso-wrap{position:relative;bottom:auto;padding:0;margin-top:16px;margin-right:0;margin-left:0}.jetpack-sso-form-display #jetpack-sso-wrap{position:relative;bottom:auto;padding:0;margin-top:0;margin-right:0;margin-left:0}#loginform #jetpack-sso-wrap p{color:#777;margin-bottom:16px}#jetpack-sso-wrap a{display:block;width:100%;text-align:center;text-decoration:none}#jetpack-sso-wrap .jetpack-sso-toggle.wpcom{display:none}.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.wpcom{display:block}.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.default{display:none}.jetpack-sso-form-display #loginform>div,.jetpack-sso-form-display #loginform>p{display:none}.jetpack-sso-form-display #loginform #jetpack-sso-wrap{display:block}.jetpack-sso-form-display #loginform{padding:26px 24px}.jetpack-sso-or{margin-bottom:16px;position:relative;text-align:center}.jetpack-sso-or:before{background:#e5e5e5;content:'';height:1px;position:absolute;right:0;top:50%;width:100%}.jetpack-sso-or span{background:#fff;color:#777;position:relative;padding:0 8px;text-transform:uppercase}.jetpack-sso.button{height:36px;line-height:34px;float:none;margin-bottom:16px;position:relative;width:100%}.jetpack-sso.button>span{position:relative;padding-right:30px}.jetpack-sso.button .genericon-wordpress{position:absolute;right:0;top:-3px;font-size:24px}@media screen and (max-width:782px){.jetpack-sso.button{line-height:22px}}#jetpack-sso-wrap__user img{border-radius:50%;display:block;margin:0 auto 16px}#jetpack-sso-wrap__user h2{font-size:21px;font-weight:300;margin-bottom:16px;text-align:center}#jetpack-sso-wrap__user h2 span{font-weight:700}.jetpack-sso-wrap__reauth{margin-bottom:16px}.jetpack-sso-form-display #nav{display:none}.jetpack-sso-form-display #backtoblog{margin:24px 0 0}.jetpack-sso-clear:after{content:"";display:table;clear:both} \ No newline at end of file
diff --git a/plugins/jetpack/modules/sso/jetpack-sso-login.css b/plugins/jetpack/modules/sso/jetpack-sso-login.css
new file mode 100644
index 00000000..3160bcb6
--- /dev/null
+++ b/plugins/jetpack/modules/sso/jetpack-sso-login.css
@@ -0,0 +1,176 @@
+#loginform {
+ /* We set !important because sometimes static is added inline */
+ position: relative !important;
+ padding-bottom: 92px;
+}
+
+.jetpack-sso-repositioned #loginform {
+ padding-bottom: 26px;
+}
+
+#loginform #jetpack-sso-wrap,
+#loginform #jetpack-sso-wrap * {
+ box-sizing: border-box;
+}
+
+
+#jetpack-sso-wrap__action,
+#jetpack-sso-wrap__user{
+ display: none;
+}
+
+.jetpack-sso-form-display #jetpack-sso-wrap__action,
+.jetpack-sso-form-display #jetpack-sso-wrap__user {
+ display: block;
+}
+
+#jetpack-sso-wrap {
+ position: absolute;
+ bottom: 20px;
+ padding: 0 24px;
+ margin-left: -24px;
+ margin-right: -24px;
+ width: 100%;
+}
+
+.jetpack-sso-repositioned #jetpack-sso-wrap {
+ position: relative;
+ bottom: auto;
+ padding: 0;
+ margin-top: 16px;
+ margin-left: 0;
+ margin-right: 0;
+}
+
+.jetpack-sso-form-display #jetpack-sso-wrap {
+ position: relative;
+ bottom: auto;
+ padding: 0;
+ margin-top: 0;
+ margin-left: 0;
+ margin-right: 0;
+}
+
+#loginform #jetpack-sso-wrap p {
+ color: #777777;
+ margin-bottom: 16px;
+}
+
+#jetpack-sso-wrap a {
+ display: block;
+ width: 100%;
+ text-align: center;
+ text-decoration: none;
+}
+
+#jetpack-sso-wrap .jetpack-sso-toggle.wpcom {
+ display: none;
+}
+
+.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.wpcom {
+ display: block;
+}
+
+
+.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.default {
+ display: none;
+}
+
+
+.jetpack-sso-form-display #loginform > p,
+.jetpack-sso-form-display #loginform > div {
+ display: none;
+}
+
+.jetpack-sso-form-display #loginform #jetpack-sso-wrap {
+ display: block;
+}
+
+.jetpack-sso-form-display #loginform {
+ padding: 26px 24px;
+}
+
+.jetpack-sso-or {
+ margin-bottom: 16px;
+ position: relative;
+ text-align: center;
+}
+
+.jetpack-sso-or:before {
+ background: #E5E5E5;
+ content: '';
+ height: 1px;
+ position: absolute;
+ left: 0;
+ top: 50%;
+ width: 100%;
+}
+.jetpack-sso-or span {
+ background: #fff;
+ color: #777;
+ position: relative;
+ padding: 0 8px;
+ text-transform: uppercase
+}
+
+.jetpack-sso.button {
+ height: 36px;
+ line-height: 34px;
+ float: none;
+ margin-bottom: 16px;
+ position: relative;
+ width: 100%;
+}
+
+.jetpack-sso.button > span {
+ position: relative;
+ padding-left: 30px;
+}
+
+.jetpack-sso.button .genericon-wordpress {
+ position: absolute;
+ left: 0;
+ top: -3px;
+ font-size: 24px;
+}
+
+@media screen and ( max-width: 782px ) {
+ .jetpack-sso.button {
+ line-height: 22px;
+ }
+}
+
+#jetpack-sso-wrap__user img {
+ border-radius: 50%;
+ display: block;
+ margin: 0 auto 16px;
+}
+
+#jetpack-sso-wrap__user h2 {
+ font-size: 21px;
+ font-weight: 300;
+ margin-bottom: 16px;
+ text-align: center;
+}
+
+#jetpack-sso-wrap__user h2 span {
+ font-weight: bold;
+}
+
+.jetpack-sso-wrap__reauth {
+ margin-bottom: 16px;
+}
+
+.jetpack-sso-form-display #nav {
+ display: none;
+}
+
+.jetpack-sso-form-display #backtoblog {
+ margin: 24px 0 0;
+}
+
+.jetpack-sso-clear:after {
+ content: "";
+ display: table;
+ clear: both;
+}
diff --git a/plugins/jetpack/modules/sso/jetpack-sso-login.js b/plugins/jetpack/modules/sso/jetpack-sso-login.js
new file mode 100644
index 00000000..a37feeb5
--- /dev/null
+++ b/plugins/jetpack/modules/sso/jetpack-sso-login.js
@@ -0,0 +1,32 @@
+jQuery( document ).ready( function( $ ) {
+ var body = $( 'body' ),
+ toggleSSO = $( '.jetpack-sso-toggle' ),
+ userLogin = $( '#user_login' ),
+ ssoWrap = $( '#jetpack-sso-wrap' ),
+ loginForm = $( '#loginform' ),
+ overflow = $( '<div class="jetpack-sso-clear"></div>' );
+
+ // The overflow div is a poor man's clearfloat. We reposition the remember me
+ // checkbox and the submit button within that to clear the float on the
+ // remember me checkbox. This is important since we're positioning the SSO
+ // UI under the submit button.
+ //
+ // @TODO: Remove this approach once core ticket 28528 is in and we have more actions in wp-login.php.
+ // See - https://core.trac.wordpress.org/ticket/28528
+ loginForm.append( overflow );
+ overflow.append( $( 'p.forgetmenot' ), $( 'p.submit' ) );
+
+ // We reposition the SSO UI at the bottom of the login form which
+ // fixes a tab order issue. Then we override any styles for absolute
+ // positioning of the SSO UI.
+ loginForm.append( ssoWrap );
+ body.addClass( 'jetpack-sso-repositioned' );
+
+ toggleSSO.on( 'click', function( e ) {
+ e.preventDefault();
+ body.toggleClass( 'jetpack-sso-form-display' );
+ if ( ! body.hasClass( 'jetpack-sso-form-display' ) ) {
+ userLogin.focus();
+ }
+ } );
+} );
diff --git a/plugins/jetpack/modules/sso/jetpack-sso-login.min.css b/plugins/jetpack/modules/sso/jetpack-sso-login.min.css
new file mode 100644
index 00000000..eeaeff27
--- /dev/null
+++ b/plugins/jetpack/modules/sso/jetpack-sso-login.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+#loginform{position:relative!important;padding-bottom:92px}.jetpack-sso-repositioned #loginform{padding-bottom:26px}#loginform #jetpack-sso-wrap,#loginform #jetpack-sso-wrap *{box-sizing:border-box}#jetpack-sso-wrap__action,#jetpack-sso-wrap__user{display:none}.jetpack-sso-form-display #jetpack-sso-wrap__action,.jetpack-sso-form-display #jetpack-sso-wrap__user{display:block}#jetpack-sso-wrap{position:absolute;bottom:20px;padding:0 24px;margin-left:-24px;margin-right:-24px;width:100%}.jetpack-sso-repositioned #jetpack-sso-wrap{position:relative;bottom:auto;padding:0;margin-top:16px;margin-left:0;margin-right:0}.jetpack-sso-form-display #jetpack-sso-wrap{position:relative;bottom:auto;padding:0;margin-top:0;margin-left:0;margin-right:0}#loginform #jetpack-sso-wrap p{color:#777;margin-bottom:16px}#jetpack-sso-wrap a{display:block;width:100%;text-align:center;text-decoration:none}#jetpack-sso-wrap .jetpack-sso-toggle.wpcom{display:none}.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.wpcom{display:block}.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.default{display:none}.jetpack-sso-form-display #loginform>div,.jetpack-sso-form-display #loginform>p{display:none}.jetpack-sso-form-display #loginform #jetpack-sso-wrap{display:block}.jetpack-sso-form-display #loginform{padding:26px 24px}.jetpack-sso-or{margin-bottom:16px;position:relative;text-align:center}.jetpack-sso-or:before{background:#e5e5e5;content:'';height:1px;position:absolute;left:0;top:50%;width:100%}.jetpack-sso-or span{background:#fff;color:#777;position:relative;padding:0 8px;text-transform:uppercase}.jetpack-sso.button{height:36px;line-height:34px;float:none;margin-bottom:16px;position:relative;width:100%}.jetpack-sso.button>span{position:relative;padding-left:30px}.jetpack-sso.button .genericon-wordpress{position:absolute;left:0;top:-3px;font-size:24px}@media screen and (max-width:782px){.jetpack-sso.button{line-height:22px}}#jetpack-sso-wrap__user img{border-radius:50%;display:block;margin:0 auto 16px}#jetpack-sso-wrap__user h2{font-size:21px;font-weight:300;margin-bottom:16px;text-align:center}#jetpack-sso-wrap__user h2 span{font-weight:700}.jetpack-sso-wrap__reauth{margin-bottom:16px}.jetpack-sso-form-display #nav{display:none}.jetpack-sso-form-display #backtoblog{margin:24px 0 0}.jetpack-sso-clear:after{content:"";display:table;clear:both} \ No newline at end of file
diff --git a/plugins/jetpack/modules/stats.php b/plugins/jetpack/modules/stats.php
new file mode 100644
index 00000000..56ff817d
--- /dev/null
+++ b/plugins/jetpack/modules/stats.php
@@ -0,0 +1,1751 @@
+<?php
+/**
+ * Module Name: Site Stats
+ * Module Description: Collect valuable traffic stats and insights.
+ * Sort Order: 1
+ * Recommendation Order: 2
+ * First Introduced: 1.1
+ * Requires Connection: Yes
+ * Auto Activate: Yes
+ * Module Tags: Site Stats, Recommended
+ * Feature: Engagement
+ * Additional Search Queries: statistics, tracking, analytics, views, traffic, stats
+ *
+ * @package Jetpack
+ */
+
+if ( defined( 'STATS_VERSION' ) ) {
+ return;
+}
+
+define( 'STATS_VERSION', '9' );
+defined( 'STATS_DASHBOARD_SERVER' ) or define( 'STATS_DASHBOARD_SERVER', 'dashboard.wordpress.com' );
+
+add_action( 'jetpack_modules_loaded', 'stats_load' );
+
+/**
+ * Load Stats.
+ *
+ * @access public
+ * @return void
+ */
+function stats_load() {
+ Jetpack::enable_module_configurable( __FILE__ );
+
+ // Generate the tracking code after wp() has queried for posts.
+ add_action( 'template_redirect', 'stats_template_redirect', 1 );
+
+ add_action( 'wp_head', 'stats_admin_bar_head', 100 );
+
+ add_action( 'wp_head', 'stats_hide_smile_css' );
+
+ add_action( 'jetpack_admin_menu', 'stats_admin_menu' );
+
+ // Map stats caps.
+ add_filter( 'map_meta_cap', 'stats_map_meta_caps', 10, 3 );
+
+ if ( isset( $_GET['oldwidget'] ) ) {
+ // Old one.
+ add_action( 'wp_dashboard_setup', 'stats_register_dashboard_widget' );
+ } else {
+ add_action( 'admin_init', 'stats_merged_widget_admin_init' );
+ }
+
+ add_filter( 'jetpack_xmlrpc_methods', 'stats_xmlrpc_methods' );
+
+ add_filter( 'pre_option_db_version', 'stats_ignore_db_version' );
+
+ // Add an icon to see stats in WordPress.com for a particular post
+ add_action( 'admin_print_styles-edit.php', 'jetpack_stats_load_admin_css' );
+ add_filter( 'manage_posts_columns', 'jetpack_stats_post_table' );
+ add_filter( 'manage_pages_columns', 'jetpack_stats_post_table' );
+ add_action( 'manage_posts_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
+ add_action( 'manage_pages_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
+}
+
+/**
+ * Delay conditional for current_user_can to after init.
+ *
+ * @access public
+ * @return void
+ */
+function stats_merged_widget_admin_init() {
+ if ( current_user_can( 'view_stats' ) ) {
+ add_action( 'load-index.php', 'stats_enqueue_dashboard_head' );
+ add_action( 'wp_dashboard_setup', 'stats_register_widget_control_callback' ); // Hacky but works.
+ add_action( 'jetpack_dashboard_widget', 'stats_jetpack_dashboard_widget' );
+ }
+}
+
+/**
+ * Enqueue Stats Dashboard
+ *
+ * @access public
+ * @return void
+ */
+function stats_enqueue_dashboard_head() {
+ add_action( 'admin_head', 'stats_dashboard_head' );
+}
+
+/**
+ * Checks if filter is set and dnt is enabled.
+ *
+ * @return bool
+ */
+function jetpack_is_dnt_enabled() {
+ /**
+ * Filter the option which decides honor DNT or not.
+ *
+ * @module stats
+ * @since 6.1.0
+ *
+ * @param bool false Honors DNT for clients who don't want to be tracked. Defaults to false. Set to true to enable.
+ */
+ if ( false === apply_filters( 'jetpack_honor_dnt_header_for_stats', false ) ) {
+ return false;
+ }
+
+ foreach ( $_SERVER as $name => $value ) {
+ if ( 'http_dnt' == strtolower( $name ) && 1 == $value ) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Prevent sparkline img requests being redirected to upgrade.php.
+ * See wp-admin/admin.php where it checks $wp_db_version.
+ *
+ * @access public
+ * @param mixed $version Version.
+ * @return string $version.
+ */
+function stats_ignore_db_version( $version ) {
+ if (
+ is_admin() &&
+ isset( $_GET['page'] ) && 'stats' === $_GET['page'] &&
+ isset( $_GET['chart'] ) && strpos($_GET['chart'], 'admin-bar-hours') === 0
+ ) {
+ global $wp_db_version;
+ return $wp_db_version;
+ }
+ return $version;
+}
+
+/**
+ * Maps view_stats cap to read cap as needed.
+ *
+ * @access public
+ * @param mixed $caps Caps.
+ * @param mixed $cap Cap.
+ * @param mixed $user_id User ID.
+ * @return array Possibly mapped capabilities for meta capability.
+ */
+function stats_map_meta_caps( $caps, $cap, $user_id ) {
+ // Map view_stats to exists.
+ if ( 'view_stats' === $cap ) {
+ $user = new WP_User( $user_id );
+ $user_role = array_shift( $user->roles );
+ $stats_roles = stats_get_option( 'roles' );
+
+ // Is the users role in the available stats roles?
+ if ( is_array( $stats_roles ) && in_array( $user_role, $stats_roles ) ) {
+ $caps = array( 'read' );
+ }
+ }
+
+ return $caps;
+}
+
+/**
+ * Stats Template Redirect.
+ *
+ * @access public
+ * @return void
+ */
+function stats_template_redirect() {
+ global $current_user, $rendered_stats_footer;
+
+ if ( is_feed() || is_robots() || is_trackback() || is_preview() || jetpack_is_dnt_enabled() ) {
+ return;
+ }
+
+ // Should we be counting this user's views?
+ if ( ! empty( $current_user->ID ) ) {
+ $count_roles = stats_get_option( 'count_roles' );
+ if ( ! is_array( $count_roles ) || ! array_intersect( $current_user->roles, $count_roles ) ) {
+ return;
+ }
+ }
+
+ add_action( 'wp_footer', 'stats_footer', 101 );
+ add_action( 'wp_head', 'stats_add_shutdown_action' );
+
+ $rendered_stats_footer = false;
+}
+
+
+/**
+ * Stats Build View Data.
+ *
+ * @access public
+ * @return array.
+ */
+function stats_build_view_data() {
+ global $wp_the_query;
+
+ $blog = Jetpack_Options::get_option( 'id' );
+ $tz = get_option( 'gmt_offset' );
+ $v = 'ext';
+ $blog_url = wp_parse_url( site_url() );
+ $srv = $blog_url['host'];
+ $j = sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION );
+ if ( $wp_the_query->is_single || $wp_the_query->is_page || $wp_the_query->is_posts_page ) {
+ // Store and reset the queried_object and queried_object_id
+ // Otherwise, redirect_canonical() will redirect to home_url( '/' ) for show_on_front = page sites where home_url() is not all lowercase.
+ // Repro:
+ // 1. Set home_url = https://ExamPle.com/
+ // 2. Set show_on_front = page
+ // 3. Set page_on_front = something
+ // 4. Visit https://example.com/ !
+ $queried_object = ( isset( $wp_the_query->queried_object ) ) ? $wp_the_query->queried_object : null;
+ $queried_object_id = ( isset( $wp_the_query->queried_object_id ) ) ? $wp_the_query->queried_object_id : null;
+ $post = $wp_the_query->get_queried_object_id();
+ $wp_the_query->queried_object = $queried_object;
+ $wp_the_query->queried_object_id = $queried_object_id;
+ } else {
+ $post = '0';
+ }
+
+ return compact( 'v', 'j', 'blog', 'post', 'tz', 'srv' );
+}
+
+/**
+ * Stats Add Shutdown Action.
+ *
+ * @access public
+ * @return void
+ */
+function stats_add_shutdown_action() {
+ // Just in case wp_footer isn't in your theme.
+ add_action( 'shutdown', 'stats_footer', 101 );
+}
+
+/**
+ * Stats Footer.
+ *
+ * @access public
+ * @return void
+ */
+function stats_footer() {
+ global $rendered_stats_footer;
+
+ if ( ! $rendered_stats_footer ) {
+ $data = stats_build_view_data();
+ if ( Jetpack_AMP_Support::is_amp_request() ) {
+ stats_render_amp_footer( $data );
+ } else {
+ stats_render_footer( $data );
+ }
+ $rendered_stats_footer = true;
+ }
+}
+
+function stats_render_footer( $data ) {
+ $script = 'https://stats.wp.com/e-' . gmdate( 'YW' ) . '.js';
+ $data_stats_array = stats_array( $data );
+
+ $stats_footer = <<<END
+<script type='text/javascript' src='{$script}' async='async' defer='defer'></script>
+<script type='text/javascript'>
+ _stq = window._stq || [];
+ _stq.push([ 'view', {{$data_stats_array}} ]);
+ _stq.push([ 'clickTrackerInit', '{$data['blog']}', '{$data['post']}' ]);
+</script>
+
+END;
+ print $stats_footer;
+}
+
+function stats_render_amp_footer( $data ) {
+ $data['host'] = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; // input var ok.
+ $data['rand'] = 'RANDOM'; // AMP placeholder.
+ $data['ref'] = 'DOCUMENT_REFERRER'; // AMP placeholder.
+ $data = array_map( 'rawurlencode', $data );
+ $pixel_url = add_query_arg( $data, 'https://pixel.wp.com/g.gif' );
+
+ ?>
+ <amp-pixel src="<?php echo esc_url( $pixel_url ); ?>"></amp-pixel>
+ <?php
+}
+
+/**
+ * Stats Get Options.
+ *
+ * @access public
+ * @return array.
+ */
+function stats_get_options() {
+ $options = get_option( 'stats_options' );
+
+ if ( ! isset( $options['version'] ) || $options['version'] < STATS_VERSION ) {
+ $options = stats_upgrade_options( $options );
+ }
+
+ return $options;
+}
+
+/**
+ * Get Stats Options.
+ *
+ * @access public
+ * @param mixed $option Option.
+ * @return mixed|null.
+ */
+function stats_get_option( $option ) {
+ $options = stats_get_options();
+
+ if ( 'blog_id' === $option ) {
+ return Jetpack_Options::get_option( 'id' );
+ }
+
+ if ( isset( $options[ $option ] ) ) {
+ return $options[ $option ];
+ }
+
+ return null;
+}
+
+/**
+ * Stats Set Options.
+ *
+ * @access public
+ * @param mixed $option Option.
+ * @param mixed $value Value.
+ * @return bool.
+ */
+function stats_set_option( $option, $value ) {
+ $options = stats_get_options();
+
+ $options[ $option ] = $value;
+
+ return stats_set_options( $options );
+}
+
+/**
+ * Stats Set Options.
+ *
+ * @access public
+ * @param mixed $options Options.
+ * @return bool
+ */
+function stats_set_options( $options ) {
+ return update_option( 'stats_options', $options );
+}
+
+/**
+ * Stats Upgrade Options.
+ *
+ * @access public
+ * @param mixed $options Options.
+ * @return array|bool
+ */
+function stats_upgrade_options( $options ) {
+ $defaults = array(
+ 'admin_bar' => true,
+ 'roles' => array( 'administrator' ),
+ 'count_roles' => array(),
+ 'blog_id' => Jetpack_Options::get_option( 'id' ),
+ 'do_not_track' => true, // @todo
+ 'hide_smile' => true,
+ );
+
+ if ( isset( $options['reg_users'] ) ) {
+ if ( ! function_exists( 'get_editable_roles' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/user.php';
+ }
+ if ( $options['reg_users'] ) {
+ $options['count_roles'] = array_keys( get_editable_roles() );
+ }
+ unset( $options['reg_users'] );
+ }
+
+ if ( is_array( $options ) && ! empty( $options ) ) {
+ $new_options = array_merge( $defaults, $options );
+ } else { $new_options = $defaults;
+ }
+
+ $new_options['version'] = STATS_VERSION;
+
+ if ( ! stats_set_options( $new_options ) ) {
+ return false;
+ }
+
+ stats_update_blog();
+
+ return $new_options;
+}
+
+/**
+ * Stats Array.
+ *
+ * @access public
+ * @param mixed $kvs KVS.
+ * @return array
+ */
+function stats_array( $kvs ) {
+ /**
+ * Filter the options added to the JavaScript Stats tracking code.
+ *
+ * @module stats
+ *
+ * @since 1.1.0
+ *
+ * @param array $kvs Array of options about the site and page you're on.
+ */
+ $kvs = apply_filters( 'stats_array', $kvs );
+ $kvs = array_map( 'addslashes', $kvs );
+ foreach ( $kvs as $k => $v ) {
+ $jskvs[] = "$k:'$v'";
+ }
+ return join( ',', $jskvs );
+}
+
+/**
+ * Admin Pages.
+ *
+ * @access public
+ * @return void
+ */
+function stats_admin_menu() {
+ global $pagenow;
+
+ // If we're at an old Stats URL, redirect to the new one.
+ // Don't even bother with caps, menu_page_url(), etc. Just do it.
+ if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'stats' === $_GET['page'] ) {
+ $redirect_url = str_replace( array( '/wp-admin/index.php?', '/wp-admin/?' ), '/wp-admin/admin.php?', $_SERVER['REQUEST_URI'] );
+ $relative_pos = strpos( $redirect_url, '/wp-admin/' );
+ if ( false !== $relative_pos ) {
+ wp_safe_redirect( admin_url( substr( $redirect_url, $relative_pos + 10 ) ) );
+ exit;
+ }
+ }
+
+ $hook = add_submenu_page( 'jetpack', __( 'Site Stats', 'jetpack' ), __( 'Site Stats', 'jetpack' ), 'view_stats', 'stats', 'jetpack_admin_ui_stats_report_page_wrapper' );
+ add_action( "load-$hook", 'stats_reports_load' );
+}
+
+/**
+ * Stats Admin Path.
+ *
+ * @access public
+ * @return string
+ */
+function stats_admin_path() {
+ return Jetpack::module_configuration_url( __FILE__ );
+}
+
+/**
+ * Stats Reports Load.
+ *
+ * @access public
+ * @return void
+ */
+function stats_reports_load() {
+ wp_enqueue_script( 'jquery' );
+ wp_enqueue_script( 'postbox' );
+ wp_enqueue_script( 'underscore' );
+
+ Jetpack_Admin_Page::load_wrapper_styles();
+ add_action( 'admin_print_styles', 'stats_reports_css' );
+
+ if ( isset( $_GET['nojs'] ) && $_GET['nojs'] ) {
+ $parsed = wp_parse_url( admin_url() );
+ // Remember user doesn't want JS.
+ setcookie( 'stnojs', '1', time() + 172800, $parsed['path'] ); // 2 days.
+ }
+
+ if ( isset( $_COOKIE['stnojs'] ) && $_COOKIE['stnojs'] ) {
+ // Detect if JS is on. If so, remove cookie so next page load is via JS.
+ add_action( 'admin_print_footer_scripts', 'stats_js_remove_stnojs_cookie' );
+ } else if ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) ) {
+ // Normal page load. Load page content via JS.
+ add_action( 'admin_print_footer_scripts', 'stats_js_load_page_via_ajax' );
+ }
+}
+
+/**
+ * Stats Reports CSS.
+ *
+ * @access public
+ * @return void
+ */
+function stats_reports_css() {
+?>
+<style type="text/css">
+#jp-stats-wrap {
+ max-width: 1040px;
+ margin: 0 auto;
+ overflow: hidden;
+}
+
+#stats-loading-wrap p {
+ text-align: center;
+ font-size: 2em;
+ margin: 7.5em 15px 0 0;
+ height: 64px;
+ line-height: 64px;
+}
+</style>
+<?php
+}
+
+
+/**
+ * Detect if JS is on. If so, remove cookie so next page load is via JS.
+ *
+ * @access public
+ * @return void
+ */
+function stats_js_remove_stnojs_cookie() {
+ $parsed = wp_parse_url( admin_url() );
+?>
+<script type="text/javascript">
+/* <![CDATA[ */
+document.cookie = 'stnojs=0; expires=Wed, 9 Mar 2011 16:55:50 UTC; path=<?php echo esc_js( $parsed['path'] ); ?>';
+/* ]]> */
+</script>
+<?php
+}
+
+/**
+ * Normal page load. Load page content via JS.
+ *
+ * @access public
+ * @return void
+ */
+function stats_js_load_page_via_ajax() {
+?>
+<script type="text/javascript">
+/* <![CDATA[ */
+if ( -1 == document.location.href.indexOf( 'noheader' ) ) {
+ jQuery( function( $ ) {
+ $.get( document.location.href + '&noheader', function( responseText ) {
+ $( '#stats-loading-wrap' ).replaceWith( responseText );
+ } );
+ } );
+}
+/* ]]> */
+</script>
+<?php
+}
+
+function jetpack_admin_ui_stats_report_page_wrapper() {
+ if( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) {
+ Jetpack_Admin_Page::wrap_ui( 'stats_reports_page', array( 'is-wide' => true ) );
+ } else {
+ stats_reports_page();
+ }
+
+}
+
+/**
+ * Stats Report Page.
+ *
+ * @access public
+ * @param bool $main_chart_only (default: false) Main Chart Only.
+ */
+function stats_reports_page( $main_chart_only = false ) {
+
+ if ( isset( $_GET['dashboard'] ) ) {
+ return stats_dashboard_widget_content();
+ }
+
+ $blog_id = stats_get_option( 'blog_id' );
+ $domain = Jetpack::build_raw_urls( get_home_url() );
+
+ $jetpack_admin_url = admin_url() . 'admin.php?page=jetpack';
+
+ if ( ! $main_chart_only && ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) {
+ $nojs_url = add_query_arg( 'nojs', '1' );
+ $http = is_ssl() ? 'https' : 'http';
+ // Loading message. No JS fallback message.
+?>
+
+ <div id="jp-stats-wrap">
+ <div class="wrap">
+ <h2><?php esc_html_e( 'Site Stats', 'jetpack' ); ?>
+ <?php
+ if ( current_user_can( 'jetpack_manage_modules' ) ) :
+ $i18n_headers = jetpack_get_module_i18n( 'stats' );
+ ?>
+ <a
+ style="font-size:13px;"
+ href="<?php echo esc_url( admin_url( 'admin.php?page=jetpack#/settings?term=' . rawurlencode( $i18n_headers['name'] ) ) ); ?>"
+ >
+ <?php esc_html_e( 'Configure', 'jetpack' ); ?>
+ </a>
+ <?php
+ endif;
+ ?>
+ </h2>
+ </div>
+ <div id="stats-loading-wrap" class="wrap">
+ <p class="hide-if-no-js"><img width="32" height="32" alt="<?php esc_attr_e( 'Loading&hellip;', 'jetpack' ); ?>" src="<?php
+ echo esc_url(
+ /**
+ * Sets external resource URL.
+ *
+ * @module stats
+ *
+ * @since 1.4.0
+ *
+ * @param string $args URL of external resource.
+ */
+ apply_filters( 'jetpack_static_url', "{$http}://en.wordpress.com/i/loading/loading-64.gif" )
+ ); ?>" /></p>
+ <p style="font-size: 11pt; margin: 0;"><a href="https://wordpress.com/stats/<?php echo esc_attr( $domain ); ?>" target="_blank"><?php esc_html_e( 'View stats on WordPress.com right now', 'jetpack' ); ?></a></p>
+ <p class="hide-if-js"><?php esc_html_e( 'Your Site Stats work better with JavaScript enabled.', 'jetpack' ); ?><br />
+ <a href="<?php echo esc_url( $nojs_url ); ?>"><?php esc_html_e( 'View Site Stats without JavaScript', 'jetpack' ); ?></a>.</p>
+ </div>
+ </div>
+<?php
+ return;
+ }
+
+ $day = isset( $_GET['day'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_GET['day'] ) ? $_GET['day'] : false;
+ $q = array(
+ 'noheader' => 'true',
+ 'proxy' => '',
+ 'page' => 'stats',
+ 'day' => $day,
+ 'blog' => $blog_id,
+ 'charset' => get_option( 'blog_charset' ),
+ 'color' => get_user_option( 'admin_color' ),
+ 'ssl' => is_ssl(),
+ 'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
+ );
+ if ( get_locale() !== 'en_US' ) {
+ $q['jp_lang'] = get_locale();
+ }
+ // Only show the main chart, without extra header data, or metaboxes.
+ $q['main_chart_only'] = $main_chart_only;
+ $args = array(
+ 'view' => array( 'referrers', 'postviews', 'searchterms', 'clicks', 'post', 'table' ),
+ 'numdays' => 'int',
+ 'day' => 'date',
+ 'unit' => array( 1, 7, 31, 'human' ),
+ 'humanize' => array( 'true' ),
+ 'num' => 'int',
+ 'summarize' => null,
+ 'post' => 'int',
+ 'width' => 'int',
+ 'height' => 'int',
+ 'data' => 'data',
+ 'blog_subscribers' => 'int',
+ 'comment_subscribers' => null,
+ 'type' => array( 'wpcom', 'email', 'pending' ),
+ 'pagenum' => 'int',
+ );
+ foreach ( $args as $var => $vals ) {
+ if ( ! isset( $_REQUEST[$var] ) )
+ continue;
+ if ( is_array( $vals ) ) {
+ if ( in_array( $_REQUEST[$var], $vals ) )
+ $q[$var] = $_REQUEST[$var];
+ } elseif ( 'int' === $vals ) {
+ $q[$var] = intval( $_REQUEST[$var] );
+ } elseif ( 'date' === $vals ) {
+ if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_REQUEST[$var] ) )
+ $q[$var] = $_REQUEST[$var];
+ } elseif ( null === $vals ) {
+ $q[$var] = '';
+ } elseif ( 'data' === $vals ) {
+ if ( 'index.php' === substr( $_REQUEST[$var], 0, 9 ) )
+ $q[$var] = $_REQUEST[$var];
+ }
+ }
+
+ if ( isset( $_GET['chart'] ) ) {
+ if ( preg_match( '/^[a-z0-9-]+$/', $_GET['chart'] ) ) {
+ $chart = sanitize_title( $_GET['chart'] );
+ $url = 'https://' . STATS_DASHBOARD_SERVER . "/wp-includes/charts/{$chart}.php";
+ }
+ } else {
+ $url = 'https://' . STATS_DASHBOARD_SERVER . "/wp-admin/index.php";
+ }
+
+ $url = add_query_arg( $q, $url );
+ $method = 'GET';
+ $timeout = 90;
+ $user_id = JETPACK_MASTER_USER; // means send the wp.com user_id
+
+ $get = Jetpack_Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
+ $get_code = wp_remote_retrieve_response_code( $get );
+ if ( is_wp_error( $get ) || ( 2 !== intval( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
+ stats_print_wp_remote_error( $get, $url );
+ } else {
+ if ( ! empty( $get['headers']['content-type'] ) ) {
+ $type = $get['headers']['content-type'];
+ if ( substr( $type, 0, 5 ) === 'image' ) {
+ $img = $get['body'];
+ header( 'Content-Type: ' . $type );
+ header( 'Content-Length: ' . strlen( $img ) );
+ echo $img;
+ die();
+ }
+ }
+ $body = stats_convert_post_titles( $get['body'] );
+ $body = stats_convert_chart_urls( $body );
+ $body = stats_convert_image_urls( $body );
+ $body = stats_convert_admin_urls( $body );
+ echo $body;
+ }
+
+ if ( isset( $_GET['page'] ) && 'stats' === $_GET['page'] && ! isset( $_GET['chart'] ) ) {
+ JetpackTracking::record_user_event( 'wpa_page_view', array( 'path' => 'old_stats' ) );
+ }
+
+ if ( isset( $_GET['noheader'] ) ) {
+ die;
+ }
+}
+
+/**
+ * Stats Convert Admin Urls.
+ *
+ * @access public
+ * @param mixed $html HTML.
+ * @return string
+ */
+function stats_convert_admin_urls( $html ) {
+ return str_replace( 'index.php?page=stats', 'admin.php?page=stats', $html );
+}
+
+/**
+ * Stats Convert Image URLs.
+ *
+ * @access public
+ * @param mixed $html HTML.
+ * @return string
+ */
+function stats_convert_image_urls( $html ) {
+ $url = set_url_scheme( 'https://' . STATS_DASHBOARD_SERVER );
+ $html = preg_replace( '|(["\'])(/i/stats.+)\\1|', '$1' . $url . '$2$1', $html );
+ return $html;
+}
+
+/**
+ * Callback for preg_replace_callback used in stats_convert_chart_urls()
+ *
+ * @since 5.6.0
+ *
+ * @param array $matches The matches resulting from the preg_replace_callback call.
+ * @return string The admin url for the chart.
+ */
+function jetpack_stats_convert_chart_urls_callback( $matches ) {
+ // If there is a query string, change the beginning '?' to a '&' so it fits into the middle of this query string.
+ return 'admin.php?page=stats&noheader&chart=' . $matches[1] . str_replace( '?', '&', $matches[2] );
+}
+
+/**
+ * Stats Convert Chart URLs.
+ *
+ * @access public
+ * @param mixed $html HTML.
+ * @return string
+ */
+function stats_convert_chart_urls( $html ) {
+ $html = preg_replace_callback(
+ '|https?://[-.a-z0-9]+/wp-includes/charts/([-.a-z0-9]+).php(\??)|',
+ 'jetpack_stats_convert_chart_urls_callback',
+ $html
+ );
+ return $html;
+}
+
+/**
+ * Stats Convert Post Title HTML
+ *
+ * @access public
+ * @param mixed $html HTML.
+ * @return string
+ */
+function stats_convert_post_titles( $html ) {
+ global $stats_posts;
+ $pattern = "<span class='post-(\d+)-link'>.*?</span>";
+ if ( ! preg_match_all( "!$pattern!", $html, $matches ) ) {
+ return $html;
+ }
+ $posts = get_posts(
+ array(
+ 'include' => implode( ',', $matches[1] ),
+ 'post_type' => 'any',
+ 'post_status' => 'any',
+ 'numberposts' => -1,
+ 'suppress_filters' => false,
+ )
+ );
+ foreach ( $posts as $post ) {
+ $stats_posts[ $post->ID ] = $post;
+ }
+ $html = preg_replace_callback( "!$pattern!", 'stats_convert_post_title', $html );
+ return $html;
+}
+
+/**
+ * Stats Convert Post Title Matches.
+ *
+ * @access public
+ * @param mixed $matches Matches.
+ * @return string
+ */
+function stats_convert_post_title( $matches ) {
+ global $stats_posts;
+ $post_id = $matches[1];
+ if ( isset( $stats_posts[$post_id] ) )
+ return '<a href="' . get_permalink( $post_id ) . '" target="_blank">' . get_the_title( $post_id ) . '</a>';
+ return $matches[0];
+}
+
+/**
+ * Stats Hide Smile.
+ *
+ * @access public
+ * @return void
+ */
+function stats_hide_smile_css() {
+ $options = stats_get_options();
+ if ( isset( $options['hide_smile'] ) && $options['hide_smile'] ) {
+?>
+<style type='text/css'>img#wpstats{display:none}</style><?php
+ }
+}
+
+/**
+ * Stats Admin Bar Head.
+ *
+ * @access public
+ * @return void
+ */
+function stats_admin_bar_head() {
+ if ( ! stats_get_option( 'admin_bar' ) )
+ return;
+
+ if ( ! current_user_can( 'view_stats' ) )
+ return;
+
+ if ( ! is_admin_bar_showing() ) {
+ return;
+ }
+
+ add_action( 'admin_bar_menu', 'stats_admin_bar_menu', 100 );
+?>
+
+<style type='text/css'>
+#wpadminbar .quicklinks li#wp-admin-bar-stats {
+ height: 32px;
+}
+#wpadminbar .quicklinks li#wp-admin-bar-stats a {
+ height: 32px;
+ padding: 0;
+}
+#wpadminbar .quicklinks li#wp-admin-bar-stats a div {
+ height: 32px;
+ width: 95px;
+ overflow: hidden;
+ margin: 0 10px;
+}
+#wpadminbar .quicklinks li#wp-admin-bar-stats a:hover div {
+ width: auto;
+ margin: 0 8px 0 10px;
+}
+#wpadminbar .quicklinks li#wp-admin-bar-stats a img {
+ height: 24px;
+ margin: 4px 0;
+ max-width: none;
+ border: none;
+}
+</style>
+<?php
+}
+
+/**
+ * Stats AdminBar.
+ *
+ * @access public
+ * @param mixed $wp_admin_bar WPAdminBar.
+ * @return void
+ */
+function stats_admin_bar_menu( &$wp_admin_bar ) {
+ $url = add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) ); // no menu_page_url() blog-side.
+
+ $img_src = esc_attr( add_query_arg( array( 'noheader' => '', 'proxy' => '', 'chart' => 'admin-bar-hours-scale' ), $url ) );
+ $img_src_2x = esc_attr( add_query_arg( array( 'noheader' => '', 'proxy' => '', 'chart' => 'admin-bar-hours-scale-2x' ), $url ) );
+
+ $alt = esc_attr( __( 'Stats', 'jetpack' ) );
+
+ $title = esc_attr( __( 'Views over 48 hours. Click for more Site Stats.', 'jetpack' ) );
+
+ $menu = array(
+ 'id' => 'stats',
+ 'href' => $url,
+ );
+ if ( Jetpack_AMP_Support::is_amp_request() ) {
+ $menu['title'] = "<amp-img src='$img_src_2x' width=112 height=24 layout=fixed alt='$alt' title='$title'></amp-img>";
+ } else {
+ $menu['title'] = "<div><script type='text/javascript'>var src;if(typeof(window.devicePixelRatio)=='undefined'||window.devicePixelRatio<2){src='$img_src';}else{src='$img_src_2x';}document.write('<img src=\''+src+'\' alt=\'$alt\' title=\'$title\' />');</script></div>";
+ }
+
+ $wp_admin_bar->add_menu( $menu );
+}
+
+/**
+ * Stats Update Blog.
+ *
+ * @access public
+ * @return void
+ */
+function stats_update_blog() {
+ Jetpack::xmlrpc_async_call( 'jetpack.updateBlog', stats_get_blog() );
+}
+
+/**
+ * Stats Get Blog.
+ *
+ * @access public
+ * @return string
+ */
+function stats_get_blog() {
+ $home = parse_url( trailingslashit( get_option( 'home' ) ) );
+ $blog = array(
+ 'host' => $home['host'],
+ 'path' => $home['path'],
+ 'blogname' => get_option( 'blogname' ),
+ 'blogdescription' => get_option( 'blogdescription' ),
+ 'siteurl' => get_option( 'siteurl' ),
+ 'gmt_offset' => get_option( 'gmt_offset' ),
+ 'timezone_string' => get_option( 'timezone_string' ),
+ 'stats_version' => STATS_VERSION,
+ 'stats_api' => 'jetpack',
+ 'page_on_front' => get_option( 'page_on_front' ),
+ 'permalink_structure' => get_option( 'permalink_structure' ),
+ 'category_base' => get_option( 'category_base' ),
+ 'tag_base' => get_option( 'tag_base' ),
+ );
+ $blog = array_merge( stats_get_options(), $blog );
+ unset( $blog['roles'], $blog['blog_id'] );
+ return stats_esc_html_deep( $blog );
+}
+
+/**
+ * Modified from stripslashes_deep()
+ *
+ * @access public
+ * @param mixed $value Value.
+ * @return string
+ */
+function stats_esc_html_deep( $value ) {
+ if ( is_array( $value ) ) {
+ $value = array_map( 'stats_esc_html_deep', $value );
+ } elseif ( is_object( $value ) ) {
+ $vars = get_object_vars( $value );
+ foreach ( $vars as $key => $data ) {
+ $value->{$key} = stats_esc_html_deep( $data );
+ }
+ } elseif ( is_string( $value ) ) {
+ $value = esc_html( $value );
+ }
+
+ return $value;
+}
+
+/**
+ * Stats xmlrpc_methods function.
+ *
+ * @access public
+ * @param mixed $methods Methods.
+ * @return array
+ */
+function stats_xmlrpc_methods( $methods ) {
+ $my_methods = array(
+ 'jetpack.getBlog' => 'stats_get_blog',
+ );
+
+ return array_merge( $methods, $my_methods );
+}
+
+/**
+ * Register Stats Dashboard Widget.
+ *
+ * @access public
+ * @return void
+ */
+function stats_register_dashboard_widget() {
+ if ( ! current_user_can( 'view_stats' ) )
+ return;
+
+ // With wp_dashboard_empty: we load in the content after the page load via JS.
+ wp_add_dashboard_widget( 'dashboard_stats', __( 'Site Stats', 'jetpack' ), 'wp_dashboard_empty', 'stats_dashboard_widget_control' );
+
+ add_action( 'admin_head', 'stats_dashboard_head' );
+}
+
+/**
+ * Stats Dashboard Widget Options.
+ *
+ * @access public
+ * @return array
+ */
+function stats_dashboard_widget_options() {
+ $defaults = array( 'chart' => 1, 'top' => 1, 'search' => 7 );
+ if ( ( ! $options = get_option( 'stats_dashboard_widget' ) ) || ! is_array( $options ) ) {
+ $options = array();
+ }
+
+ // Ignore obsolete option values.
+ $intervals = array( 1, 7, 31, 90, 365 );
+ foreach ( array( 'top', 'search' ) as $key ) {
+ if ( isset( $options[ $key ] ) && ! in_array( $options[ $key ], $intervals ) ) {
+ unset( $options[ $key ] );
+ }
+ }
+
+ return array_merge( $defaults, $options );
+}
+
+/**
+ * Stats Dashboard Widget Control.
+ *
+ * @access public
+ * @return void
+ */
+function stats_dashboard_widget_control() {
+ $periods = array(
+ '1' => __( 'day', 'jetpack' ),
+ '7' => __( 'week', 'jetpack' ),
+ '31' => __( 'month', 'jetpack' ),
+ );
+ $intervals = array(
+ '1' => __( 'the past day', 'jetpack' ),
+ '7' => __( 'the past week', 'jetpack' ),
+ '31' => __( 'the past month', 'jetpack' ),
+ '90' => __( 'the past quarter', 'jetpack' ),
+ '365' => __( 'the past year', 'jetpack' ),
+ );
+ $defaults = array(
+ 'top' => 1,
+ 'search' => 7,
+ );
+
+ $options = stats_dashboard_widget_options();
+
+ if ( 'post' === strtolower( $_SERVER['REQUEST_METHOD'] ) && isset( $_POST['widget_id'] ) && 'dashboard_stats' === $_POST['widget_id'] ) {
+ if ( isset( $periods[ $_POST['chart'] ] ) ) {
+ $options['chart'] = $_POST['chart'];
+ }
+ foreach ( array( 'top', 'search' ) as $key ) {
+ if ( isset( $intervals[ $_POST[ $key ] ] ) ) {
+ $options[ $key ] = $_POST[ $key ];
+ } else { $options[ $key ] = $defaults[ $key ];
+ }
+ }
+ update_option( 'stats_dashboard_widget', $options );
+ }
+?>
+ <p>
+ <label for="chart"><?php esc_html_e( 'Chart stats by' , 'jetpack' ); ?></label>
+ <select id="chart" name="chart">
+ <?php
+ foreach ( $periods as $val => $label ) {
+?>
+ <option value="<?php echo $val; ?>"<?php selected( $val, $options['chart'] ); ?>><?php echo esc_html( $label ); ?></option>
+ <?php
+ }
+?>
+ </select>.
+ </p>
+
+ <p>
+ <label for="top"><?php esc_html_e( 'Show top posts over', 'jetpack' ); ?></label>
+ <select id="top" name="top">
+ <?php
+ foreach ( $intervals as $val => $label ) {
+?>
+ <option value="<?php echo $val; ?>"<?php selected( $val, $options['top'] ); ?>><?php echo esc_html( $label ); ?></option>
+ <?php
+ }
+?>
+ </select>.
+ </p>
+
+ <p>
+ <label for="search"><?php esc_html_e( 'Show top search terms over', 'jetpack' ); ?></label>
+ <select id="search" name="search">
+ <?php
+ foreach ( $intervals as $val => $label ) {
+?>
+ <option value="<?php echo $val; ?>"<?php selected( $val, $options['search'] ); ?>><?php echo esc_html( $label ); ?></option>
+ <?php
+ }
+?>
+ </select>.
+ </p>
+ <?php
+}
+
+/**
+ * Jetpack Stats Dashboard Widget.
+ *
+ * @access public
+ * @return void
+ */
+function stats_jetpack_dashboard_widget() {
+?>
+ <form id="stats_dashboard_widget_control" action="<?php echo esc_url( admin_url() ); ?>" method="post">
+ <?php stats_dashboard_widget_control(); ?>
+ <?php wp_nonce_field( 'edit-dashboard-widget_dashboard_stats', 'dashboard-widget-nonce' ); ?>
+ <input type="hidden" name="widget_id" value="dashboard_stats" />
+ <?php submit_button( __( 'Submit', 'jetpack' ) ); ?>
+ </form>
+ <span class="js-toggle-stats_dashboard_widget_control">
+ <?php esc_html_e( 'Configure', 'jetpack' ); ?>
+ </span>
+ <div id="dashboard_stats">
+ <div class="inside">
+ <div style="height: 250px;"></div>
+ </div>
+ </div>
+ <script>
+ jQuery(document).ready(function($){
+ var $toggle = $('.js-toggle-stats_dashboard_widget_control');
+
+ $toggle.parent().prev().append( $toggle );
+ $toggle.show().click(function(e){
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ $(this).parent().toggleClass('controlVisible');
+ $('#stats_dashboard_widget_control').slideToggle();
+ });
+ });
+ </script>
+ <style>
+ .js-toggle-stats_dashboard_widget_control {
+ display: none;
+ float: right;
+ margin-top: 0.2em;
+ font-weight: 400;
+ color: #444;
+ font-size: .8em;
+ text-decoration: underline;
+ cursor: pointer;
+ }
+ #stats_dashboard_widget_control {
+ display: none;
+ padding: 0 10px;
+ overflow: hidden;
+ }
+ #stats_dashboard_widget_control .button-primary {
+ float: right;
+ }
+ #dashboard_stats {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 0 10px;
+ }
+ </style>
+ <?php
+}
+
+/**
+ * Register Stats Widget Control Callback.
+ *
+ * @access public
+ * @return void
+ */
+function stats_register_widget_control_callback() {
+ $GLOBALS['wp_dashboard_control_callbacks']['dashboard_stats'] = 'stats_dashboard_widget_control';
+}
+
+/**
+ * JavaScript and CSS for dashboard widget.
+ *
+ * @access public
+ * @return void
+ */
+function stats_dashboard_head() { ?>
+<script type="text/javascript">
+/* <![CDATA[ */
+jQuery( function($) {
+ var dashStats = jQuery( '#dashboard_stats div.inside' );
+
+ if ( dashStats.find( '.dashboard-widget-control-form' ).length ) {
+ return;
+ }
+
+ if ( ! dashStats.length ) {
+ dashStats = jQuery( '#dashboard_stats div.dashboard-widget-content' );
+ var h = parseInt( dashStats.parent().height() ) - parseInt( dashStats.prev().height() );
+ var args = 'width=' + dashStats.width() + '&height=' + h.toString();
+ } else {
+ if ( jQuery('#dashboard_stats' ).hasClass('postbox') ) {
+ var args = 'width=' + ( dashStats.prev().width() * 2 ).toString();
+ } else {
+ var args = 'width=' + ( dashStats.width() * 2 ).toString();
+ }
+ }
+
+ dashStats
+ .not( '.dashboard-widget-control' )
+ .load( 'admin.php?page=stats&noheader&dashboard&' + args );
+
+ jQuery( window ).one( 'resize', function() {
+ jQuery( '#stat-chart' ).css( 'width', 'auto' );
+ } );
+} );
+/* ]]> */
+</script>
+<style type="text/css">
+/* <![CDATA[ */
+#stat-chart {
+ background: none !important;
+}
+#dashboard_stats .inside {
+ margin: 10px 0 0 0 !important;
+}
+#dashboard_stats #stats-graph {
+ margin: 0;
+}
+#stats-info {
+ border-top: 1px solid #dfdfdf;
+ margin: 7px -10px 0 -10px;
+ padding: 10px;
+ background: #fcfcfc;
+ -moz-box-shadow:inset 0 1px 0 #fff;
+ -webkit-box-shadow:inset 0 1px 0 #fff;
+ box-shadow:inset 0 1px 0 #fff;
+ overflow: hidden;
+ border-radius: 0 0 2px 2px;
+ -webkit-border-radius: 0 0 2px 2px;
+ -moz-border-radius: 0 0 2px 2px;
+ -khtml-border-radius: 0 0 2px 2px;
+}
+#stats-info #top-posts, #stats-info #top-search {
+ float: left;
+ width: 50%;
+}
+#stats-info #top-posts {
+ padding-right: 3%;
+}
+#top-posts .stats-section-inner p {
+ white-space: nowrap;
+ overflow: hidden;
+}
+#top-posts .stats-section-inner p a {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+#stats-info div#active {
+ border-top: 1px solid #dfdfdf;
+ margin: 0 -10px;
+ padding: 10px 10px 0 10px;
+ -moz-box-shadow:inset 0 1px 0 #fff;
+ -webkit-box-shadow:inset 0 1px 0 #fff;
+ box-shadow:inset 0 1px 0 #fff;
+ overflow: hidden;
+}
+#top-search p {
+ color: #999;
+}
+#stats-info h3 {
+ font-size: 1em;
+ margin: 0 0 .5em 0 !important;
+}
+#stats-info p {
+ margin: 0 0 .25em;
+ color: #999;
+}
+#stats-info p.widget-loading {
+ margin: 1em 0 0;
+ color: #333;
+}
+#stats-info p a {
+ display: block;
+}
+#stats-info p a.button {
+ display: inline;
+}
+/* ]]> */
+</style>
+<?php
+}
+
+/**
+ * Stats Dashboard Widget Content.
+ *
+ * @access public
+ * @return void
+ */
+function stats_dashboard_widget_content() {
+ if ( ! isset( $_GET['width'] ) || ( ! $width = (int) ( $_GET['width'] / 2 ) ) || $width < 250 ) {
+ $width = 370;
+ }
+ if ( ! isset( $_GET['height'] ) || ( ! $height = (int) $_GET['height'] - 36 ) || $height < 230 ) {
+ $height = 180;
+ }
+
+ $_width = $width - 5;
+ $_height = $height - ( $GLOBALS['is_winIE'] ? 16 : 5 ); // Hack!
+
+ $options = stats_dashboard_widget_options();
+ $blog_id = Jetpack_Options::get_option( 'id' );
+
+ $q = array(
+ 'noheader' => 'true',
+ 'proxy' => '',
+ 'blog' => $blog_id,
+ 'page' => 'stats',
+ 'chart' => '',
+ 'unit' => $options['chart'],
+ 'color' => get_user_option( 'admin_color' ),
+ 'width' => $_width,
+ 'height' => $_height,
+ 'ssl' => is_ssl(),
+ 'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
+ );
+
+ $url = 'https://' . STATS_DASHBOARD_SERVER . "/wp-admin/index.php";
+
+ $url = add_query_arg( $q, $url );
+ $method = 'GET';
+ $timeout = 90;
+ $user_id = JETPACK_MASTER_USER;
+
+ $get = Jetpack_Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
+ $get_code = wp_remote_retrieve_response_code( $get );
+ if ( is_wp_error( $get ) || ( 2 !== intval( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
+ stats_print_wp_remote_error( $get, $url );
+ } else {
+ $body = stats_convert_post_titles( $get['body'] );
+ $body = stats_convert_chart_urls( $body );
+ $body = stats_convert_image_urls( $body );
+ echo $body;
+ }
+
+ $post_ids = array();
+
+ $csv_end_date = date( 'Y-m-d', current_time( 'timestamp' ) );
+ $csv_args = array( 'top' => "&limit=8&end=$csv_end_date", 'search' => "&limit=5&end=$csv_end_date" );
+ /* Translators: Stats dashboard widget postviews list: "$post_title $views Views". */
+ $printf = __( '%1$s %2$s Views' , 'jetpack' );
+
+ foreach ( $top_posts = stats_get_csv( 'postviews', "days=$options[top]$csv_args[top]" ) as $i => $post ) {
+ if ( 0 === $post['post_id'] ) {
+ unset( $top_posts[$i] );
+ continue;
+ }
+ $post_ids[] = $post['post_id'];
+ }
+
+ // Cache.
+ get_posts( array( 'include' => join( ',', array_unique( $post_ids ) ) ) );
+
+ $searches = array();
+ foreach ( $search_terms = stats_get_csv( 'searchterms', "days=$options[search]$csv_args[search]" ) as $search_term ) {
+ if ( 'encrypted_search_terms' === $search_term['searchterm'] ) {
+ continue;
+ }
+ $searches[] = esc_html( $search_term['searchterm'] );
+ }
+
+?>
+<div id="stats-info">
+ <div id="top-posts" class='stats-section'>
+ <div class="stats-section-inner">
+ <h3 class="heading"><?php esc_html_e( 'Top Posts' , 'jetpack' ); ?></h3>
+ <?php
+ if ( empty( $top_posts ) ) {
+?>
+ <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
+ <?php
+ } else {
+ foreach ( $top_posts as $post ) {
+ if ( ! get_post( $post['post_id'] ) ) {
+ continue;
+ }
+?>
+ <p><?php printf(
+ $printf,
+ '<a href="' . get_permalink( $post['post_id'] ) . '">' . get_the_title( $post['post_id'] ) . '</a>',
+ number_format_i18n( $post['views'] )
+ ); ?></p>
+ <?php
+ }
+ }
+?>
+ </div>
+ </div>
+ <div id="top-search" class='stats-section'>
+ <div class="stats-section-inner">
+ <h3 class="heading"><?php esc_html_e( 'Top Searches' , 'jetpack' ); ?></h3>
+ <?php
+ if ( empty( $searches ) ) {
+?>
+ <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
+ <?php
+ } else {
+ foreach ( $searches as $search_term_item ) {
+ printf(
+ '<p>%s</p>',
+ $search_term_item
+ );
+ }
+ }
+?>
+ </div>
+ </div>
+</div>
+<div class="clear"></div>
+<div class="stats-view-all">
+<?php
+ printf(
+ '<a class="button" target="_blank" rel="noopener noreferrer" href="%1$s">%2$s</a>',
+ esc_url( "https://wordpress.com/stats/day/" . Jetpack::build_raw_urls( get_home_url() ) ),
+ esc_html__( 'View all stats', 'jetpack' )
+ );
+?>
+</div>
+<div class="clear"></div>
+<?php
+ exit;
+}
+
+/**
+ * Stats Print WP Remote Error.
+ *
+ * @access public
+ * @param mixed $get Get.
+ * @param mixed $url URL.
+ * @return void
+ */
+function stats_print_wp_remote_error( $get, $url ) {
+ $state_name = 'stats_remote_error_' . substr( md5( $url ), 0, 8 );
+ $previous_error = Jetpack::state( $state_name );
+ $error = md5( serialize( compact( 'get', 'url' ) ) );
+ Jetpack::state( $state_name, $error );
+ if ( $error !== $previous_error ) {
+?>
+ <div class="wrap">
+ <p><?php esc_html_e( 'We were unable to get your stats just now. Please reload this page to try again.', 'jetpack' ); ?></p>
+ </div>
+<?php
+ return;
+ }
+?>
+ <div class="wrap">
+ <p><?php printf( __( 'We were unable to get your stats just now. Please reload this page to try again. If this error persists, please <a href="%1$s" target="_blank">contact support</a>. In your report please include the information below.', 'jetpack' ), 'https://support.wordpress.com/contact/?jetpack=needs-service' ); ?></p>
+ <pre>
+ User Agent: "<?php echo esc_html( $_SERVER['HTTP_USER_AGENT'] ); ?>"
+ Page URL: "http<?php echo (is_ssl()?'s':'') . '://' . esc_html( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); ?>"
+ API URL: "<?php echo esc_url( $url ); ?>"
+<?php
+if ( is_wp_error( $get ) ) {
+ foreach ( $get->get_error_codes() as $code ) {
+ foreach ( $get->get_error_messages( $code ) as $message ) {
+?>
+<?php print $code . ': "' . $message . '"' ?>
+
+<?php
+ }
+ }
+} else {
+ $get_code = wp_remote_retrieve_response_code( $get );
+ $content_length = strlen( wp_remote_retrieve_body( $get ) );
+?>
+Response code: "<?php print $get_code ?>"
+Content length: "<?php print $content_length ?>"
+
+<?php
+}
+ ?></pre>
+ </div>
+ <?php
+}
+
+/**
+ * Get stats from WordPress.com
+ *
+ * @param string $table The stats which you want to retrieve: postviews, or searchterms.
+ * @param array $args {
+ * An associative array of arguments.
+ *
+ * @type bool $end The last day of the desired time frame. Format is 'Y-m-d' (e.g. 2007-05-01)
+ * and default timezone is UTC date. Default value is Now.
+ * @type string $days The length of the desired time frame. Default is 30. Maximum 90 days.
+ * @type int $limit The maximum number of records to return. Default is 10. Maximum 100.
+ * @type int $post_id The ID of the post to retrieve stats data for
+ * @type string $summarize If present, summarizes all matching records. Default Null.
+ *
+ * }
+ *
+ * @return array {
+ * An array of post view data, each post as an array
+ *
+ * array {
+ * The post view data for a single post
+ *
+ * @type string $post_id The ID of the post
+ * @type string $post_title The title of the post
+ * @type string $post_permalink The permalink for the post
+ * @type string $views The number of views for the post within the $num_days specified
+ * }
+ * }
+ */
+function stats_get_csv( $table, $args = null ) {
+ $defaults = array( 'end' => false, 'days' => false, 'limit' => 3, 'post_id' => false, 'summarize' => '' );
+
+ $args = wp_parse_args( $args, $defaults );
+ $args['table'] = $table;
+ $args['blog_id'] = Jetpack_Options::get_option( 'id' );
+
+ $stats_csv_url = add_query_arg( $args, 'https://stats.wordpress.com/csv.php' );
+
+ $key = md5( $stats_csv_url );
+
+ // Get cache.
+ $stats_cache = get_option( 'stats_cache' );
+ if ( ! $stats_cache || ! is_array( $stats_cache ) ) {
+ $stats_cache = array();
+ }
+
+ // Return or expire this key.
+ if ( isset( $stats_cache[ $key ] ) ) {
+ $time = key( $stats_cache[ $key ] );
+ if ( time() - $time < 300 ) {
+ return $stats_cache[ $key ][ $time ];
+ }
+ unset( $stats_cache[ $key ] );
+ }
+
+ $stats_rows = array();
+ do {
+ if ( ! $stats = stats_get_remote_csv( $stats_csv_url ) ) {
+ break;
+ }
+
+ $labels = array_shift( $stats );
+
+ if ( 0 === stripos( $labels[0], 'error' ) ) {
+ break;
+ }
+
+ $stats_rows = array();
+ for ( $s = 0; isset( $stats[ $s ] ); $s++ ) {
+ $row = array();
+ foreach ( $labels as $col => $label ) {
+ $row[ $label ] = $stats[ $s ][ $col ];
+ }
+ $stats_rows[] = $row;
+ }
+ } while ( 0 );
+
+ // Expire old keys.
+ foreach ( $stats_cache as $k => $cache ) {
+ if ( ! is_array( $cache ) || 300 < time() - key( $cache ) ) {
+ unset( $stats_cache[ $k ] );
+ }
+ }
+
+ // Set cache.
+ $stats_cache[ $key ] = array( time() => $stats_rows );
+ update_option( 'stats_cache', $stats_cache );
+
+ return $stats_rows;
+}
+
+/**
+ * Stats get remote CSV.
+ *
+ * @access public
+ * @param mixed $url URL.
+ * @return array
+ */
+function stats_get_remote_csv( $url ) {
+ $method = 'GET';
+ $timeout = 90;
+ $user_id = JETPACK_MASTER_USER;
+
+ $get = Jetpack_Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
+ $get_code = wp_remote_retrieve_response_code( $get );
+ if ( is_wp_error( $get ) || ( 2 !== intval( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
+ return array(); // @todo: return an error?
+ } else {
+ return stats_str_getcsv( $get['body'] );
+ }
+}
+
+/**
+ * Rather than parsing the csv and its special cases, we create a new file and do fgetcsv on it.
+ *
+ * @access public
+ * @param mixed $csv CSV.
+ * @return array.
+ */
+function stats_str_getcsv( $csv ) {
+ if ( function_exists( 'str_getcsv' ) ) {
+ $lines = str_getcsv( $csv, "\n" ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.str_getcsvFound
+ return array_map( 'str_getcsv', $lines );
+ }
+ if ( ! $temp = tmpfile() ) { // The tmpfile() automatically unlinks.
+ return false;
+ }
+
+ $data = array();
+
+ fwrite( $temp, $csv, strlen( $csv ) );
+ fseek( $temp, 0 );
+ while ( false !== $row = fgetcsv( $temp, 2000 ) ) {
+ $data[] = $row;
+ }
+ fclose( $temp );
+
+ return $data;
+}
+
+/**
+ * Abstract out building the rest api stats path.
+ *
+ * @param string $resource Resource.
+ * @return string
+ */
+function jetpack_stats_api_path( $resource = '' ) {
+ $resource = ltrim( $resource, '/' );
+ return sprintf( '/sites/%d/stats/%s', stats_get_option( 'blog_id' ), $resource );
+}
+
+/**
+ * Fetches stats data from the REST API. Caches locally for 5 minutes.
+ *
+ * @link: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/
+ * @access public
+ * @param array $args (default: array()) The args that are passed to the endpoint.
+ * @param string $resource (default: '') Optional sub-endpoint following /stats/.
+ * @return array|WP_Error.
+ */
+function stats_get_from_restapi( $args = array(), $resource = '' ) {
+ $endpoint = jetpack_stats_api_path( $resource );
+ $api_version = '1.1';
+ $args = wp_parse_args( $args, array() );
+ $cache_key = md5( implode( '|', array( $endpoint, $api_version, serialize( $args ) ) ) );
+
+ // Get cache.
+ $stats_cache = Jetpack_Options::get_option( 'restapi_stats_cache', array() );
+ if ( ! is_array( $stats_cache ) ) {
+ $stats_cache = array();
+ }
+
+ // Return or expire this key.
+ if ( isset( $stats_cache[ $cache_key ] ) ) {
+ $time = key( $stats_cache[ $cache_key ] );
+ if ( time() - $time < ( 5 * MINUTE_IN_SECONDS ) ) {
+ $cached_stats = $stats_cache[ $cache_key ][ $time ];
+ if ( is_wp_error( $cached_stats ) ) {
+ return $cached_stats;
+ }
+ $cached_stats = (object) array_merge( array( 'cached_at' => $time ), (array) $cached_stats );
+ return $cached_stats;
+ }
+ unset( $stats_cache[ $cache_key ] );
+ }
+
+ // Do the dirty work.
+ $response = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, $api_version, $args );
+ if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ $data = is_wp_error( $response ) ? $response : new WP_Error( 'stats_error' );
+ } else {
+ $data = json_decode( wp_remote_retrieve_body( $response ) );
+ }
+
+ // Expire old keys.
+ foreach ( $stats_cache as $k => $cache ) {
+ if ( ! is_array( $cache ) || ( 5 * MINUTE_IN_SECONDS ) < time() - key( $cache ) ) {
+ unset( $stats_cache[ $k ] );
+ }
+ }
+
+ // Set cache.
+ $stats_cache[ $cache_key ] = array(
+ time() => $data,
+ );
+ Jetpack_Options::update_option( 'restapi_stats_cache', $stats_cache, false );
+
+ return $data;
+}
+
+/**
+ * Load CSS needed for Stats column width in WP-Admin area.
+ *
+ * @since 4.7.0
+ */
+function jetpack_stats_load_admin_css() {
+ ?>
+ <style type="text/css">
+ .fixed .column-stats {
+ width: 5em;
+ }
+ </style>
+ <?php
+}
+
+/**
+ * Set header for column that allows to go to WordPress.com to see an entry's stats.
+ *
+ * @param array $columns An array of column names.
+ *
+ * @since 4.7.0
+ *
+ * @return mixed
+ */
+function jetpack_stats_post_table( $columns ) { // Adds a stats link on the edit posts page
+ if ( ! current_user_can( 'view_stats' ) || ! Jetpack::is_user_connected() ) {
+ return $columns;
+ }
+ // Array-Fu to add before comments
+ $pos = array_search( 'comments', array_keys( $columns ) );
+ if ( ! is_int( $pos ) ) {
+ return $columns;
+ }
+ $chunks = array_chunk( $columns, $pos, true );
+ $chunks[0]['stats'] = esc_html__( 'Stats', 'jetpack' );
+
+ return call_user_func_array( 'array_merge', $chunks );
+}
+
+/**
+ * Set content for cell with link to an entry's stats in WordPress.com.
+ *
+ * @param string $column The name of the column to display.
+ * @param int $post_id The current post ID.
+ *
+ * @since 4.7.0
+ *
+ * @return mixed
+ */
+function jetpack_stats_post_table_cell( $column, $post_id ) {
+ if ( 'stats' == $column ) {
+ if ( 'publish' != get_post_status( $post_id ) ) {
+ printf(
+ '<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
+ esc_html__( 'No stats', 'jetpack' )
+ );
+ } else {
+ printf(
+ '<a href="%s" title="%s" class="dashicons dashicons-chart-bar" target="_blank"></a>',
+ esc_url( "https://wordpress.com/stats/post/$post_id/" . Jetpack::build_raw_urls( get_home_url() ) ),
+ esc_html__( 'View stats for this post in WordPress.com', 'jetpack' )
+ );
+ }
+ }
+}
diff --git a/plugins/jetpack/modules/subscriptions.php b/plugins/jetpack/modules/subscriptions.php
new file mode 100644
index 00000000..aeda5dd4
--- /dev/null
+++ b/plugins/jetpack/modules/subscriptions.php
@@ -0,0 +1,781 @@
+<?php
+/**
+ * Module Name: Subscriptions
+ * Module Description: Allow users to subscribe to your posts and comments and receive notifications via email
+ * Sort Order: 9
+ * Recommendation Order: 8
+ * First Introduced: 1.2
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Social
+ * Feature: Engagement
+ * Additional Search Queries: subscriptions, subscription, email, follow, followers, subscribers, signup
+ */
+
+add_action( 'jetpack_modules_loaded', 'jetpack_subscriptions_load' );
+
+function jetpack_subscriptions_load() {
+ Jetpack::enable_module_configurable( __FILE__ );
+}
+
+/**
+ * Cherry picks keys from `$_SERVER` array.
+ *
+ * @since 6.0.0
+ *
+ * @return array An array of server data.
+ */
+function jetpack_subscriptions_cherry_pick_server_data() {
+ $data = array();
+
+ foreach ( $_SERVER as $key => $value ) {
+ if ( ! is_string( $value ) || 0 === strpos( $key, 'HTTP_COOKIE' ) ) {
+ continue;
+ }
+
+ if ( 0 === strpos( $key, 'HTTP_' ) || in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ), true ) ) {
+ $data[ $key ] = $value;
+ }
+ }
+
+ return $data;
+}
+
+class Jetpack_Subscriptions {
+ public $jetpack = false;
+
+ public static $hash;
+
+ /**
+ * Singleton
+ * @static
+ */
+ static function init() {
+ static $instance = false;
+
+ if ( !$instance ) {
+ $instance = new Jetpack_Subscriptions;
+ }
+
+ return $instance;
+ }
+
+ function __construct() {
+ $this->jetpack = Jetpack::init();
+
+ // Don't use COOKIEHASH as it could be shared across installs && is non-unique in multisite.
+ // @see: https://twitter.com/nacin/status/378246957451333632
+ self::$hash = md5( get_option( 'siteurl' ) );
+
+ add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
+
+ // @todo remove sync from subscriptions and move elsewhere...
+
+ // Add Configuration Page
+ add_action( 'admin_init', array( $this, 'configure' ) );
+
+ // Catch subscription widget submits
+ if ( isset( $_REQUEST['jetpack_subscriptions_widget'] ) )
+ add_action( 'template_redirect', array( $this, 'widget_submit' ) );
+
+ // Set up the comment subscription checkboxes
+ add_filter( 'comment_form_submit_field', array( $this, 'comment_subscribe_init' ), 10, 2 );
+
+ // Catch comment posts and check for subscriptions.
+ add_action( 'comment_post', array( $this, 'comment_subscribe_submit' ), 50, 2 );
+
+ // Adds post meta checkbox in the post submit metabox
+ add_action( 'post_submitbox_misc_actions', array( $this, 'subscription_post_page_metabox' ) );
+
+ add_action( 'transition_post_status', array( $this, 'maybe_send_subscription_email' ), 10, 3 );
+
+ add_filter( 'jetpack_published_post_flags', array( $this, 'set_post_flags' ), 10, 2 );
+
+ add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 18, 1 );
+ }
+
+ /**
+ * Jetpack_Subscriptions::xmlrpc_methods()
+ *
+ * Register subscriptions methods with the Jetpack XML-RPC server.
+ * @param array $methods
+ */
+ function xmlrpc_methods( $methods ) {
+ return array_merge(
+ $methods,
+ array(
+ 'jetpack.subscriptions.subscribe' => array( $this, 'subscribe' ),
+ )
+ );
+ }
+
+ /*
+ * Disable Subscribe on Single Post
+ * Register post meta
+ */
+ function subscription_post_page_metabox() {
+ if (
+ /**
+ * Filter whether or not to show the per-post subscription option.
+ *
+ * @module subscriptions
+ *
+ * @since 3.7.0
+ *
+ * @param bool true = show checkbox option on all new posts | false = hide the option.
+ */
+ ! apply_filters( 'jetpack_allow_per_post_subscriptions', false ) )
+ {
+ return;
+ }
+
+ if ( has_filter( 'jetpack_subscriptions_exclude_these_categories' ) || has_filter( 'jetpack_subscriptions_include_only_these_categories' ) ) {
+ return;
+ }
+
+ global $post;
+ $disable_subscribe_value = get_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', true );
+ // only show checkbox if post hasn't been published and is a 'post' post type.
+ if ( get_post_status( $post->ID ) !== 'publish' && get_post_type( $post->ID ) == 'post' ) :
+ // Nonce it
+ wp_nonce_field( 'disable_subscribe', 'disable_subscribe_nonce' );
+ ?>
+ <div class="misc-pub-section">
+ <label for="_jetpack_dont_email_post_to_subs"><?php _e( 'Jetpack Subscriptions:', 'jetpack' ); ?></label><br>
+ <input type="checkbox" name="_jetpack_dont_email_post_to_subs" id="jetpack-per-post-subscribe" value="1" <?php checked( $disable_subscribe_value, 1, true ); ?> />
+ <?php _e( 'Don&#8217;t send this to subscribers', 'jetpack' ); ?>
+ </div>
+ <?php endif;
+ }
+
+ /**
+ * Checks whether or not the post should be emailed to subscribers
+ *
+ * It checks for the following things in order:
+ * - Usage of filter jetpack_subscriptions_exclude_these_categories
+ * - Usage of filter jetpack_subscriptions_include_only_these_categories
+ * - Existence of the per-post checkbox option
+ *
+ * Only one of these can be used at any given time.
+ *
+ * @param $new_status string - the "new" post status of the transition when saved
+ * @param $old_status string - the "old" post status of the transition when saved
+ * @param $post obj - The post object
+ */
+ function maybe_send_subscription_email( $new_status, $old_status, $post ) {
+
+ if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
+ return;
+ }
+
+ // Make sure that the checkbox is preseved
+ if ( ! empty( $_POST['disable_subscribe_nonce'] ) && wp_verify_nonce( $_POST['disable_subscribe_nonce'], 'disable_subscribe' ) ) {
+ $set_checkbox = isset( $_POST['_jetpack_dont_email_post_to_subs'] ) ? 1 : 0;
+ update_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', $set_checkbox );
+ }
+ }
+
+ function update_published_message( $messages ) {
+ global $post;
+ if ( ! $this->should_email_post_to_subscribers( $post ) ) {
+ return $messages;
+ }
+
+ $view_post_link_html = sprintf( ' <a href="%1$s">%2$s</a>',
+ esc_url( get_permalink( $post ) ),
+ __( 'View post', 'jetpack' )
+ );
+
+ $messages['post'][6] = sprintf(
+ /* translators: Message shown after a post is published */
+ esc_html__( 'Post published and sending emails to subscribers.', 'jetpack' )
+ ) . $view_post_link_html;
+ return $messages;
+ }
+
+ public function should_email_post_to_subscribers( $post ) {
+ $should_email = true;
+ if ( get_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', true ) ) {
+ return false;
+ }
+
+ // Only posts are currently supported
+ if ( $post->post_type !== 'post' ) {
+ return false;
+ }
+
+ // Private posts are not sent to subscribers.
+ if ( 'private' === $post->post_status ) {
+ return false;
+ }
+
+ /**
+ * Array of categories that will never trigger subscription emails.
+ *
+ * Will not send subscription emails from any post from within these categories.
+ *
+ * @module subscriptions
+ *
+ * @since 3.7.0
+ *
+ * @param array $args Array of category slugs or ID's.
+ */
+ $excluded_categories = apply_filters( 'jetpack_subscriptions_exclude_these_categories', array() );
+
+ // Never email posts from these categories
+ if ( ! empty( $excluded_categories ) && in_category( $excluded_categories, $post->ID ) ) {
+ $should_email = false;
+ }
+
+ /**
+ * ONLY send subscription emails for these categories
+ *
+ * Will ONLY send subscription emails to these categories.
+ *
+ * @module subscriptions
+ *
+ * @since 3.7.0
+ *
+ * @param array $args Array of category slugs or ID's.
+ */
+ $only_these_categories = apply_filters( 'jetpack_subscriptions_exclude_all_categories_except', array() );
+
+ // Only emails posts from these categories
+ if ( ! empty( $only_these_categories ) && ! in_category( $only_these_categories, $post->ID ) ) {
+ $should_email = false;
+ }
+
+ return $should_email;
+ }
+
+ function set_post_flags( $flags, $post ) {
+ $flags['send_subscription'] = $this->should_email_post_to_subscribers( $post );
+ return $flags;
+ }
+
+ /**
+ * Jetpack_Subscriptions::configure()
+ *
+ * Jetpack Subscriptions configuration screen.
+ */
+ function configure() {
+ // Create the section
+ add_settings_section(
+ 'jetpack_subscriptions',
+ __( 'Jetpack Subscriptions Settings', 'jetpack' ),
+ array( $this, 'subscriptions_settings_section' ),
+ 'discussion'
+ );
+
+ /** Subscribe to Posts ***************************************************/
+
+ add_settings_field(
+ 'jetpack_subscriptions_post_subscribe',
+ __( 'Follow Blog', 'jetpack' ),
+ array( $this, 'subscription_post_subscribe_setting' ),
+ 'discussion',
+ 'jetpack_subscriptions'
+ );
+
+ register_setting(
+ 'discussion',
+ 'stb_enabled'
+ );
+
+ /** Subscribe to Comments ******************************************************/
+
+ add_settings_field(
+ 'jetpack_subscriptions_comment_subscribe',
+ __( 'Follow Comments', 'jetpack' ),
+ array( $this, 'subscription_comment_subscribe_setting' ),
+ 'discussion',
+ 'jetpack_subscriptions'
+ );
+
+ register_setting(
+ 'discussion',
+ 'stc_enabled'
+ );
+
+ /** Subscription Messaging Options ******************************************************/
+
+ register_setting(
+ 'reading',
+ 'subscription_options',
+ array( $this, 'validate_settings' )
+ );
+
+ add_settings_section(
+ 'email_settings',
+ __( 'Follower Settings', 'jetpack' ),
+ array( $this, 'reading_section' ),
+ 'reading'
+ );
+
+ add_settings_field(
+ 'invitation',
+ __( 'Blog follow email text', 'jetpack' ),
+ array( $this, 'setting_invitation' ),
+ 'reading',
+ 'email_settings'
+ );
+
+ add_settings_field(
+ 'comment-follow',
+ __( 'Comment follow email text', 'jetpack' ),
+ array( $this, 'setting_comment_follow' ),
+ 'reading',
+ 'email_settings'
+ );
+ }
+
+ /**
+ * Discussions setting section blurb
+ *
+ */
+ function subscriptions_settings_section() {
+ ?>
+ <p id="jetpack-subscriptions-settings"><?php _e( 'Change whether your visitors can subscribe to your posts or comments or both.', 'jetpack' ); ?></p>
+
+ <?php
+ }
+
+ /**
+ * Post Subscriptions Toggle
+ *
+ */
+ function subscription_post_subscribe_setting() {
+
+ $stb_enabled = get_option( 'stb_enabled', 1 ); ?>
+
+ <p class="description">
+ <input type="checkbox" name="stb_enabled" id="jetpack-post-subscribe" value="1" <?php checked( $stb_enabled, 1 ); ?> />
+ <?php _e( "Show a <em>'follow blog'</em> option in the comment form", 'jetpack' ); ?>
+ </p>
+ <?php
+ }
+
+ /**
+ * Comments Subscriptions Toggle
+ *
+ */
+ function subscription_comment_subscribe_setting() {
+
+ $stc_enabled = get_option( 'stc_enabled', 1 ); ?>
+
+ <p class="description">
+ <input type="checkbox" name="stc_enabled" id="jetpack-comment-subscribe" value="1" <?php checked( $stc_enabled, 1 ); ?> />
+ <?php _e( "Show a <em>'follow comments'</em> option in the comment form", 'jetpack' ); ?>
+ </p>
+
+ <?php
+ }
+
+ function validate_settings( $settings ) {
+ global $allowedposttags;
+
+ $default = $this->get_default_settings();
+
+ // Blog Follow
+ $settings['invitation'] = trim( wp_kses( $settings['invitation'], $allowedposttags ) );
+ if ( empty( $settings['invitation'] ) )
+ $settings['invitation'] = $default['invitation'];
+
+ // Comments Follow (single post)
+ $settings['comment_follow'] = trim( wp_kses( $settings['comment_follow'], $allowedposttags ) );
+ if ( empty( $settings['comment_follow'] ) )
+ $settings['comment_follow'] = $default['comment_follow'];
+
+ return $settings;
+ }
+
+ public function reading_section() {
+ echo '<p id="follower-settings">';
+ _e( 'These settings change emails sent from your blog to followers.', 'jetpack' );
+ echo '</p>';
+ }
+
+ public function setting_invitation() {
+ $settings = $this->get_settings();
+ echo '<textarea name="subscription_options[invitation]" class="large-text" cols="50" rows="5">' . esc_textarea( $settings['invitation'] ) . '</textarea>';
+ echo '<p><span class="description">'.__( 'Introduction text sent when someone follows your blog. (Site and confirmation details will be automatically added for you.)', 'jetpack' ).'</span></p>';
+ }
+
+ public function setting_comment_follow() {
+ $settings = $this->get_settings();
+ echo '<textarea name="subscription_options[comment_follow]" class="large-text" cols="50" rows="5">' . esc_textarea( $settings['comment_follow'] ) . '</textarea>';
+ echo '<p><span class="description">'.__( 'Introduction text sent when someone follows a post on your blog. (Site and confirmation details will be automatically added for you.)', 'jetpack' ).'</span></p>';
+ }
+
+ function get_default_settings() {
+ return array(
+ 'invitation' => __( "Howdy.\n\nYou recently followed this blog's posts. This means you will receive each new post by email.\n\nTo activate, click confirm below. If you believe this is an error, ignore this message and we'll never bother you again.", 'jetpack' ),
+ 'comment_follow' => __( "Howdy.\n\nYou recently followed one of my posts. This means you will receive an email when new comments are posted.\n\nTo activate, click confirm below. If you believe this is an error, ignore this message and we'll never bother you again.", 'jetpack' )
+ );
+ }
+
+ function get_settings() {
+ return wp_parse_args( (array) get_option( 'subscription_options', array() ), $this->get_default_settings() );
+ }
+
+ /**
+ * Jetpack_Subscriptions::subscribe()
+ *
+ * Send a synchronous XML-RPC subscribe to blog posts or subscribe to post comments request.
+ *
+ * @param string $email
+ * @param array $post_ids (optional) defaults to 0 for blog posts only: array of post IDs to subscribe to blog's posts
+ * @param bool $async (optional) Should the subscription be performed asynchronously? Defaults to true.
+ *
+ * @return true|Jetpack_Error true on success
+ * invalid_email : not a valid email address
+ * invalid_post_id : not a valid post ID
+ * unknown_post_id : unknown post
+ * not_subscribed : strange error. Jetpack servers at WordPress.com could subscribe the email.
+ * disabled : Site owner has disabled subscriptions.
+ * active : Already subscribed.
+ * unknown : strange error. Jetpack servers at WordPress.com returned something malformed.
+ * unknown_status : strange error. Jetpack servers at WordPress.com returned something I didn't understand.
+ */
+ function subscribe( $email, $post_ids = 0, $async = true, $extra_data = array() ) {
+ if ( !is_email( $email ) ) {
+ return new Jetpack_Error( 'invalid_email' );
+ }
+
+ if ( !$async ) {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_ClientMulticall();
+ }
+
+ foreach ( (array) $post_ids as $post_id ) {
+ $post_id = (int) $post_id;
+ if ( $post_id < 0 ) {
+ return new Jetpack_Error( 'invalid_post_id' );
+ } else if ( $post_id && !$post = get_post( $post_id ) ) {
+ return new Jetpack_Error( 'unknown_post_id' );
+ }
+
+ if ( $async ) {
+ Jetpack::xmlrpc_async_call( 'jetpack.subscribeToSite', $email, $post_id, serialize( $extra_data ) );
+ } else {
+ $xml->addCall( 'jetpack.subscribeToSite', $email, $post_id, serialize( $extra_data ) );
+ }
+ }
+
+ if ( $async ) {
+ return;
+ }
+
+ // Call
+ $xml->query();
+
+ if ( $xml->isError() ) {
+ return $xml->get_jetpack_error();
+ }
+
+ $responses = $xml->getResponse();
+
+ $r = array();
+ foreach ( (array) $responses as $response ) {
+ if ( isset( $response['faultCode'] ) || isset( $response['faultString'] ) ) {
+ $r[] = $xml->get_jetpack_error( $response['faultCode'], $response['faultString'] );
+ continue;
+ }
+
+ if ( !is_array( $response[0] ) || empty( $response[0]['status'] ) ) {
+ $r[] = new Jetpack_Error( 'unknown' );
+ continue;
+ }
+
+ switch ( $response[0]['status'] ) {
+ case 'error' :
+ $r[] = new Jetpack_Error( 'not_subscribed' );
+ continue 2;
+ case 'disabled' :
+ $r[] = new Jetpack_Error( 'disabled' );
+ continue 2;
+ case 'active' :
+ $r[] = new Jetpack_Error( 'active' );
+ continue 2;
+ case 'pending' :
+ $r[] = true;
+ continue 2;
+ default :
+ $r[] = new Jetpack_Error( 'unknown_status', (string) $response[0]['status'] );
+ continue 2;
+ }
+ }
+
+ return $r;
+ }
+
+ /**
+ * Jetpack_Subscriptions::widget_submit()
+ *
+ * When a user submits their email via the blog subscription widget, check the details and call the subsribe() method.
+ */
+ function widget_submit() {
+ // Check the nonce.
+ if ( is_user_logged_in() ) {
+ check_admin_referer( 'blogsub_subscribe_' . get_current_blog_id() );
+ }
+
+ if ( empty( $_REQUEST['email'] ) )
+ return false;
+
+ $redirect_fragment = false;
+ if ( isset( $_REQUEST['redirect_fragment'] ) ) {
+ $redirect_fragment = preg_replace( '/[^a-z0-9_-]/i', '', $_REQUEST['redirect_fragment'] );
+ }
+ if ( !$redirect_fragment ) {
+ $redirect_fragment = 'subscribe-blog';
+ }
+
+ $subscribe = Jetpack_Subscriptions::subscribe(
+ $_REQUEST['email'],
+ 0,
+ false,
+ array(
+ 'source' => 'widget',
+ 'widget-in-use' => is_active_widget( false, false, 'blog_subscription', true ) ? 'yes' : 'no',
+ 'comment_status' => '',
+ 'server_data' => jetpack_subscriptions_cherry_pick_server_data(),
+ )
+ );
+
+ if ( is_wp_error( $subscribe ) ) {
+ $error = $subscribe->get_error_code();
+ } else {
+ $error = false;
+ foreach ( $subscribe as $response ) {
+ if ( is_wp_error( $response ) ) {
+ $error = $response->get_error_code();
+ break;
+ }
+ }
+ }
+
+ switch ( $error ) {
+ case false:
+ $result = 'success';
+ break;
+ case 'invalid_email':
+ $result = $error;
+ break;
+ case 'blocked_email':
+ $result = 'opted_out';
+ break;
+ case 'active':
+ case 'pending':
+ $result = 'already';
+ break;
+ default:
+ $result = 'error';
+ break;
+ }
+
+ $redirect = add_query_arg( 'subscribe', $result );
+
+ /**
+ * Fires on each subscription form submission.
+ *
+ * @module subscriptions
+ *
+ * @since 3.7.0
+ *
+ * @param string $result Result of form submission: success, invalid_email, already, error.
+ */
+ do_action( 'jetpack_subscriptions_form_submission', $result );
+
+ wp_safe_redirect( "$redirect#$redirect_fragment" );
+ exit;
+ }
+
+ /**
+ * Jetpack_Subscriptions::comment_subscribe_init()
+ *
+ * Set up and add the comment subscription checkbox to the comment form.
+ *
+ * @param string $submit_button HTML markup for the submit field.
+ * @param array $args Arguments passed to `comment_form()`.
+ */
+ function comment_subscribe_init( $submit_button, $args ) {
+ global $post;
+
+ $comments_checked = '';
+ $blog_checked = '';
+
+ // Check for a comment / blog submission and set a cookie to retain the setting and check the boxes.
+ if ( isset( $_COOKIE[ 'jetpack_comments_subscribe_' . self::$hash . '_' . $post->ID ] ) ) {
+ $comments_checked = ' checked="checked"';
+ }
+
+ if ( isset( $_COOKIE[ 'jetpack_blog_subscribe_' . self::$hash ] ) ) {
+ $blog_checked = ' checked="checked"';
+ }
+
+ // Some themes call this function, don't show the checkbox again
+ remove_action( 'comment_form', 'subscription_comment_form' );
+
+ // Check if Mark Jaquith's Subscribe to Comments plugin is active - if so, suppress Jetpack checkbox
+
+ $str = '';
+
+ if ( FALSE === has_filter( 'comment_form', 'show_subscription_checkbox' ) && 1 == get_option( 'stc_enabled', 1 ) && empty( $post->post_password ) && 'post' == get_post_type() ) {
+ // Subscribe to comments checkbox
+ $str .= '<p class="comment-subscription-form"><input type="checkbox" name="subscribe_comments" id="subscribe_comments" value="subscribe" style="width: auto; -moz-appearance: checkbox; -webkit-appearance: checkbox;"' . $comments_checked . ' /> ';
+ $comment_sub_text = __( 'Notify me of follow-up comments by email.', 'jetpack' );
+ $str .= '<label class="subscribe-label" id="subscribe-label" for="subscribe_comments">' . esc_html(
+ /**
+ * Filter the Subscribe to comments text appearing below the comment form.
+ *
+ * @module subscriptions
+ *
+ * @since 3.4.0
+ *
+ * @param string $comment_sub_text Subscribe to comments text.
+ */
+ apply_filters( 'jetpack_subscribe_comment_label', $comment_sub_text )
+ ) . '</label>';
+ $str .= '</p>';
+ }
+
+ if ( 1 == get_option( 'stb_enabled', 1 ) ) {
+ // Subscribe to blog checkbox
+ $str .= '<p class="comment-subscription-form"><input type="checkbox" name="subscribe_blog" id="subscribe_blog" value="subscribe" style="width: auto; -moz-appearance: checkbox; -webkit-appearance: checkbox;"' . $blog_checked . ' /> ';
+ $blog_sub_text = __( 'Notify me of new posts by email.', 'jetpack' );
+ $str .= '<label class="subscribe-label" id="subscribe-blog-label" for="subscribe_blog">' . esc_html(
+ /**
+ * Filter the Subscribe to blog text appearing below the comment form.
+ *
+ * @module subscriptions
+ *
+ * @since 3.4.0
+ *
+ * @param string $comment_sub_text Subscribe to blog text.
+ */
+ apply_filters( 'jetpack_subscribe_blog_label', $blog_sub_text )
+ ) . '</label>';
+ $str .= '</p>';
+ }
+
+ /**
+ * Filter the output of the subscription options appearing below the comment form.
+ *
+ * @module subscriptions
+ *
+ * @since 1.2.0
+ *
+ * @param string $str Comment Subscription form HTML output.
+ */
+ $str = apply_filters( 'jetpack_comment_subscription_form', $str );
+
+ return $submit_button . $str;
+ }
+
+ /**
+ * Jetpack_Subscriptions::comment_subscribe_init()
+ *
+ * When a user checks the comment subscribe box and submits a comment, subscribe them to the comment thread.
+ */
+ function comment_subscribe_submit( $comment_id, $approved ) {
+ if ( 'spam' === $approved ) {
+ return;
+ }
+
+ $comment = get_comment( $comment_id );
+
+ // Set cookies for this post/comment
+ $this->set_cookies( isset( $_REQUEST['subscribe_comments'] ), $comment->comment_post_ID, isset( $_REQUEST['subscribe_blog'] ) );
+
+ if ( !isset( $_REQUEST['subscribe_comments'] ) && !isset( $_REQUEST['subscribe_blog'] ) )
+ return;
+
+ $post_ids = array();
+
+ if ( isset( $_REQUEST['subscribe_comments'] ) )
+ $post_ids[] = $comment->comment_post_ID;
+
+ if ( isset( $_REQUEST['subscribe_blog'] ) )
+ $post_ids[] = 0;
+
+ $result = Jetpack_Subscriptions::subscribe(
+ $comment->comment_author_email,
+ $post_ids,
+ true,
+ array(
+ 'source' => 'comment-form',
+ 'widget-in-use' => is_active_widget( false, false, 'blog_subscription', true ) ? 'yes' : 'no',
+ 'comment_status' => $approved,
+ 'server_data' => jetpack_subscriptions_cherry_pick_server_data(),
+ )
+ );
+
+ /**
+ * Fires on each comment subscription form submission.
+ *
+ * @module subscriptions
+ *
+ * @since 5.5.0
+ *
+ * @param NULL|WP_Error $result Result of form submission: NULL on success, WP_Error otherwise.
+ * @param Array $post_ids An array of post IDs that the user subscribed to, 0 means blog subscription.
+ */
+ do_action( 'jetpack_subscriptions_comment_form_submission', $result, $post_ids );
+ }
+
+ /**
+ * Jetpack_Subscriptions::set_cookies()
+ *
+ * Set a cookie to save state on the comment and post subscription checkboxes.
+ *
+ * @param bool $subscribe_to_post Whether the user chose to subscribe to subsequent comments on this post.
+ * @param int $post_id If $subscribe_to_post is true, the post ID they've subscribed to.
+ * @param bool $subscribe_to_blog Whether the user chose to subscribe to all new posts on the blog.
+ */
+ function set_cookies( $subscribe_to_post = false, $post_id = null, $subscribe_to_blog = false ) {
+ $post_id = intval( $post_id );
+
+ /** This filter is already documented in core/wp-includes/comment-functions.php */
+ $cookie_lifetime = apply_filters( 'comment_cookie_lifetime', 30000000 );
+
+ /**
+ * Filter the Jetpack Comment cookie path.
+ *
+ * @module subscriptions
+ *
+ * @since 2.5.0
+ *
+ * @param string COOKIEPATH Cookie path.
+ */
+ $cookie_path = apply_filters( 'jetpack_comment_cookie_path', COOKIEPATH );
+
+ /**
+ * Filter the Jetpack Comment cookie domain.
+ *
+ * @module subscriptions
+ *
+ * @since 2.5.0
+ *
+ * @param string COOKIE_DOMAIN Cookie domain.
+ */
+ $cookie_domain = apply_filters( 'jetpack_comment_cookie_domain', COOKIE_DOMAIN );
+
+ if ( $subscribe_to_post && $post_id >= 0 ) {
+ setcookie( 'jetpack_comments_subscribe_' . self::$hash . '_' . $post_id, 1, time() + $cookie_lifetime, $cookie_path, $cookie_domain );
+ } else {
+ setcookie( 'jetpack_comments_subscribe_' . self::$hash . '_' . $post_id, '', time() - 3600, $cookie_path, $cookie_domain );
+ }
+
+ if ( $subscribe_to_blog ) {
+ setcookie( 'jetpack_blog_subscribe_' . self::$hash, 1, time() + $cookie_lifetime, $cookie_path, $cookie_domain );
+ } else {
+ setcookie( 'jetpack_blog_subscribe_' . self::$hash, '', time() - 3600, $cookie_path, $cookie_domain );
+ }
+ }
+
+}
+
+Jetpack_Subscriptions::init();
+
+include dirname( __FILE__ ) . '/subscriptions/views.php';
diff --git a/plugins/jetpack/modules/subscriptions/readme.md b/plugins/jetpack/modules/subscriptions/readme.md
new file mode 100644
index 00000000..44680150
--- /dev/null
+++ b/plugins/jetpack/modules/subscriptions/readme.md
@@ -0,0 +1,12 @@
+## Assets for Subscriptions
+
+### subscriptions.css
+
+CSS required to render the subscription widget
+
+### views.php
+
+This file handles the registration of various subscriptions
+views, i.e. a widget and a block for the post editor.
+
+This file is shared with wordpress.com
diff --git a/plugins/jetpack/modules/subscriptions/subscriptions.css b/plugins/jetpack/modules/subscriptions/subscriptions.css
new file mode 100644
index 00000000..2bd1bd23
--- /dev/null
+++ b/plugins/jetpack/modules/subscriptions/subscriptions.css
@@ -0,0 +1,29 @@
+#subscribe-email input {
+ width: 95%;
+}
+
+.comment-subscription-form {
+ margin-bottom: 1em;
+}
+
+.comment-subscription-form .subscribe-label {
+ display: inline !important;
+}
+
+/*
+Text meant only for screen readers.
+Provides support for themes that do not bundle this CSS yet.
+@see https://make.wordpress.org/accessibility/2015/02/09/hiding-text-for-screen-readers-with-wordpress-core/
+***********************************/
+.screen-reader-text {
+ border: 0;
+ clip: rect(1px, 1px, 1px, 1px);
+ clip-path: inset(50%);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute ! important;
+ width: 1px;
+ word-wrap: normal ! important;
+}
diff --git a/plugins/jetpack/modules/subscriptions/views.php b/plugins/jetpack/modules/subscriptions/views.php
new file mode 100644
index 00000000..6a332f8f
--- /dev/null
+++ b/plugins/jetpack/modules/subscriptions/views.php
@@ -0,0 +1,755 @@
+<?php
+
+class Jetpack_Subscriptions_Widget extends WP_Widget {
+ static $instance_count = 0;
+ /**
+ * @var array When printing the submit button, what tags are allowed
+ */
+ static $allowed_html_tags_for_submit_button = array( 'br' => array() );
+
+ function __construct() {
+ $widget_ops = array(
+ 'classname' => 'widget_blog_subscription jetpack_subscription_widget',
+ 'description' => __( 'Add an email signup form to allow people to subscribe to your blog.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ );
+
+ $name = self::is_jetpack() ?
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Blog Subscriptions', 'jetpack' ) ) :
+ __( 'Follow Blog', 'jetpack' );
+
+ parent::__construct(
+ 'blog_subscription',
+ $name,
+ $widget_ops
+ );
+
+ if ( self::is_jetpack() &&
+ (
+ is_active_widget( false, false, $this->id_base ) ||
+ is_active_widget( false, false, 'monster' ) ||
+ is_customize_preview()
+ )
+ ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_style' ) );
+ }
+ }
+
+ /**
+ * Enqueue the form's CSS.
+ *
+ * @since 4.5.0
+ */
+ function enqueue_style() {
+ wp_register_style(
+ 'jetpack-subscriptions',
+ plugins_url( 'subscriptions.css', __FILE__ ),
+ array(),
+ JETPACK__VERSION
+ );
+ wp_enqueue_style( 'jetpack-subscriptions' );
+ }
+
+ /**
+ * Renders a full widget either within the context of WordPress widget, or in response to a shortcode.
+ *
+ * @param array $args Display arguments including 'before_title', 'after_title', 'before_widget', and 'after_widget'.
+ * @param array $instance The settings for the particular instance of the widget.
+ */
+ function widget( $args, $instance ) {
+ if ( self::is_jetpack() &&
+ /** This filter is documented in modules/contact-form/grunion-contact-form.php */
+ false === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
+ ) {
+ $subscribe_email = '';
+ } else {
+ $current_user = wp_get_current_user();
+ if ( ! empty( $current_user->user_email ) ) {
+ $subscribe_email = esc_attr( $current_user->user_email );
+ } else {
+ $subscribe_email = '';
+ }
+ }
+
+ $stats_action = self::is_jetpack() ? 'jetpack_subscriptions' : 'follow_blog';
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', $stats_action );
+
+ $after_widget = isset( $args['after_widget'] ) ? $args['after_widget'] : '';
+ $before_widget = isset( $args['before_widget'] ) ? $args['before_widget'] : '';
+ $instance = wp_parse_args( (array) $instance, $this->defaults() );
+
+ echo $before_widget;
+
+ Jetpack_Subscriptions_Widget::$instance_count ++;
+
+ self::render_widget_title( $args, $instance );
+
+ self::render_widget_status_messages( $instance );
+
+ if ( self::is_current_user_subscribed() ) {
+ self::render_widget_already_subscribed( $instance );
+ } else {
+ self::render_widget_subscription_form( $args, $instance, $subscribe_email );
+ }
+
+ echo "\n" . $after_widget;
+ }
+
+ /**
+ * Prints the widget's title. If show_only_email_and_button is true, we will not show a title.
+ *
+ * @param array $args Display arguments including 'before_title', 'after_title', 'before_widget', and 'after_widget'.
+ * @param array $instance The settings for the particular instance of the widget.
+ */
+ static function render_widget_title( $args, $instance ) {
+ $show_only_email_and_button = $instance['show_only_email_and_button'];
+ $before_title = isset( $args['before_title'] ) ? $args['before_title'] : '';
+ $after_title = isset( $args['after_title'] ) ? $args['after_title'] : '';
+ if ( self::is_wpcom() && ! $show_only_email_and_button ) {
+ if ( self::is_current_user_subscribed() ) {
+ if ( ! empty( $instance['title_following'] ) ) {
+ echo $before_title . '<label for="subscribe-field' . ( Jetpack_Subscriptions_Widget::$instance_count > 1 ? '-' . Jetpack_Subscriptions_Widget::$instance_count : '' ) . '">' . esc_attr( $instance['title_following'] ) . '</label>' . $after_title . "\n";
+ }
+ } else {
+ if ( ! empty( $instance['title'] ) ) {
+ echo $before_title . '<label for="subscribe-field' . ( Jetpack_Subscriptions_Widget::$instance_count > 1 ? '-' . Jetpack_Subscriptions_Widget::$instance_count : '' ) . '">' . esc_attr( $instance['title'] ) . '</label>' . $after_title . "\n";
+ }
+ }
+ }
+
+ if ( self::is_jetpack() && empty( $instance['show_only_email_and_button'] ) ) {
+ echo $args['before_title'] . esc_attr( $instance['title'] ) . $args['after_title'] . "\n";
+ }
+ }
+
+ /**
+ * Prints the subscription block's status messages after someone has attempted to subscribe.
+ * Either a success message or an error message.
+ *
+ * @param array $instance The settings for the particular instance of the widget.
+ */
+ static function render_widget_status_messages( $instance ) {
+ if ( self::is_jetpack() && isset( $_GET['subscribe'] ) ) {
+ $success_message = isset( $instance['success_message'] ) ? stripslashes( $instance['success_message'] ) : '';
+ $subscribers_total = self::fetch_subscriber_count();
+ switch ( $_GET['subscribe'] ) :
+ case 'invalid_email' : ?>
+ <p class="error"><?php esc_html_e( 'The email you entered was invalid. Please check and try again.', 'jetpack' ); ?></p>
+ <?php break;
+ case 'opted_out' : ?>
+ <p class="error"><?php printf( __( 'The email address has opted out of subscription emails. <br /> You can manage your preferences at <a href="%1$s" title="%2$s" target="_blank">subscribe.wordpress.com</a>', 'jetpack' ),
+ 'https://subscribe.wordpress.com/',
+ __( 'Manage your email preferences.', 'jetpack' )
+ ); ?></p>
+ <?php break;
+ case 'already' : ?>
+ <p class="error"><?php printf( __( 'You have already subscribed to this site. Please check your inbox. <br /> You can manage your preferences at <a href="%1$s" title="%2$s" target="_blank">subscribe.wordpress.com</a>', 'jetpack' ),
+ 'https://subscribe.wordpress.com/',
+ __( 'Manage your email preferences.', 'jetpack' )
+ ); ?></p>
+ <?php break;
+ case 'success' : ?>
+ <div class="success"><?php echo wpautop( str_replace( '[total-subscribers]', number_format_i18n( $subscribers_total['value'] ), $success_message ) ); ?></div>
+ <?php break;
+ default : ?>
+ <p class="error"><?php esc_html_e( 'There was an error when subscribing. Please try again.', 'jetpack' ); ?></p>
+ <?php break;
+ endswitch;
+ }
+
+ if ( self::is_wpcom() && self::wpcom_has_status_message() ) {
+ global $themecolors;
+ switch ( $_GET['blogsub'] ) {
+ case 'confirming':
+ echo "<div style='background-color: #{$themecolors['bg']}; border: 1px solid #{$themecolors['border']}; color: #{$themecolors['text']}; padding-left: 5px; padding-right: 5px; margin-bottom: 10px;'>";
+ _e( 'Thanks for subscribing! You&rsquo;ll get an email with a link to confirm your subscription. If you don&rsquo;t get it, please <a href="http://en.support.wordpress.com/contact/">contact us</a>.' );
+ echo "</div>";
+ break;
+ case 'blocked':
+ echo "<div style='background-color: #{$themecolors['bg']}; border: 1px solid #{$themecolors['border']}; color: #{$themecolors['text']}; padding-left: 5px; padding-right: 5px; margin-bottom: 10px;'>";
+ _e( 'Subscriptions have been blocked for this email address.' );
+ echo "</div>";
+ break;
+ case 'flooded':
+ echo "<div style='background-color: #{$themecolors['bg']}; border: 1px solid #{$themecolors['border']}; color: #{$themecolors['text']}; padding-left: 5px; padding-right: 5px; margin-bottom: 10px;'>";
+ _e( 'You already have several pending email subscriptions. Approve or delete a few through your <a href="https://subscribe.wordpress.com/">Subscription Manager</a> before attempting to subscribe to more blogs.' );
+ echo "</div>";
+ break;
+ case 'spammed':
+ echo "<div style='background-color: #{$themecolors['bg']}; border: 1px solid #{$themecolors['border']}; color: #{$themecolors['text']}; padding-left: 5px; padding-right: 5px; margin-bottom: 10px;'>";
+ echo wp_kses_post( sprintf( __( 'Because there are many pending subscriptions for this email address, we have blocked the subscription. Please <a href="%s">activate or delete</a> pending subscriptions before attempting to subscribe.' ), 'https://subscribe.wordpress.com/' ) );
+ echo "</div>";
+ break;
+ case 'subscribed':
+ echo "<div style='background-color: #{$themecolors['bg']}; border: 1px solid #{$themecolors['border']}; color: #{$themecolors['text']}; padding-left: 5px; padding-right: 5px; margin-bottom: 10px;'>";
+ _e( 'You&rsquo;re already subscribed to this site.' );
+ echo "</div>";
+ break;
+ case 'pending':
+ echo "<div style='background-color: #{$themecolors['bg']}; border: 1px solid #{$themecolors['border']}; color: #{$themecolors['text']}; padding-left: 5px; padding-right: 5px; margin-bottom: 10px;'>";
+ _e( 'You have a pending subscription already; we just sent you another email. Click the link or <a href="http://en.support.wordpress.com/contact/">contact us</a> if you don&rsquo;t receive it.' );
+ echo "</div>";
+ break;
+ case 'confirmed':
+ echo "<div style='background-color: #{$themecolors['bg']}; border: 1px solid #{$themecolors['border']}; color: #{$themecolors['text']}; padding-left: 5px; padding-right: 5px; margin-bottom: 10px;'>";
+ _e( 'Congrats, you&rsquo;re subscribed! You&rsquo;ll get an email with the details of your subscription and an unsubscribe link.' );
+ echo "</div>";
+ break;
+ }
+ }
+ }
+
+ /**
+ * Renders a message to folks who are already subscribed.
+ *
+ * @param array $instance The settings for the particular instance of the widget.
+ *
+ * @return void
+ */
+ static function render_widget_already_subscribed( $instance ) {
+ if ( self::is_wpcom() ) {
+ $subscribers_total = self::fetch_subscriber_count();
+ $edit_subs_url = 'https://wordpress.com/following/edit/';
+ if ( function_exists( 'localized_wpcom_url' ) ) {
+ $edit_subs_url = localized_wpcom_url( http() . '://wordpress.com/following/edit/', get_user_locale() );
+ }
+ $show_subscribers_total = (bool) $instance['show_subscribers_total'];
+ if ( $show_subscribers_total && $subscribers_total > 1 ) :
+ $subscribers_not_me = $subscribers_total - 1;
+ /* translators: %s: number of folks following the blog */
+ ?>
+ <p><?php printf( _n( 'You are following this blog, along with %s other amazing person (<a href="%s">manage</a>).', 'You are following this blog, along with %s other amazing people (<a href="%s">manage</a>).', $subscribers_not_me ), number_format_i18n( $subscribers_not_me ), $edit_subs_url ) ?></p><?php
+ else :
+ ?>
+ <p><?php printf( __( 'You are following this blog (<a href="%s">manage</a>).' ), $edit_subs_url ) ?></p><?php
+ endif;
+ }
+ }
+
+ /**
+ * Renders a form allowing folks to subscribe to the blog.
+ *
+ * @param array $args Display arguments including 'before_title', 'after_title', 'before_widget', and 'after_widget'.
+ * @param array $instance The settings for the particular instance of the widget.
+ * @param string $subscribe_email The email to use to prefill the form.
+ */
+ static function render_widget_subscription_form( $args, $instance, $subscribe_email ) {
+ $show_only_email_and_button = $instance['show_only_email_and_button'];
+ $subscribe_logged_in = isset( $instance['subscribe_logged_in'] ) ? stripslashes( $instance['subscribe_logged_in'] ) : '';
+ $show_subscribers_total = (bool) $instance['show_subscribers_total'];
+ $subscribe_text = empty( $instance['show_only_email_and_button'] ) ?
+ stripslashes( $instance['subscribe_text'] ) :
+ false;
+ $referer = ( is_ssl() ? 'https' : 'http' ) . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+ $source = 'widget';
+ $widget_id = esc_attr( ! empty( $args['widget_id'] ) ? esc_attr( $args['widget_id'] ) : mt_rand( 450, 550 ) );
+ $subscribe_button = ! empty( $instance['submit_button_text'] ) ? $instance['submit_button_text'] : $instance['subscribe_button'];
+ $subscribers_total = self::fetch_subscriber_count();
+ $subscribe_placeholder = isset( $instance['subscribe_placeholder'] ) ? stripslashes( $instance['subscribe_placeholder'] ) : '';
+ $submit_button_classes = isset( $instance['submit_button_classes'] ) ? $instance['submit_button_classes'] : '';
+ $submit_button_styles = isset( $instance['submit_button_styles'] ) ? $instance['submit_button_styles'] : '';
+
+ if ( self::is_wpcom() && ! self::wpcom_has_status_message() ) {
+ global $current_blog;
+ $url = defined( 'SUBSCRIBE_BLOG_URL' ) ? SUBSCRIBE_BLOG_URL : '';
+ ?>
+ <form action="<?php echo $url; ?>" method="post" accept-charset="utf-8"
+ id="subscribe-blog<?php if ( Jetpack_Subscriptions_Widget::$instance_count > 1 ) {
+ echo '-' . Jetpack_Subscriptions_Widget::$instance_count;
+ } ?>">
+ <?php if ( is_user_logged_in() ) : ?>
+ <?php
+ if ( ! $show_only_email_and_button ) {
+ echo wpautop( $subscribe_logged_in );
+ }
+ if ( $show_subscribers_total && $subscribers_total ) {
+ /* translators: %s: number of folks following the blog */
+ echo wpautop( sprintf( _n( 'Join %s other follower', 'Join %s other followers', $subscribers_total ), number_format_i18n( $subscribers_total ) ) );
+ }
+ ?>
+ <?php else : ?>
+ <?php
+ if ( ! $show_only_email_and_button ) {
+ echo wpautop( $subscribe_text );
+ }
+ if ( $show_subscribers_total && $subscribers_total ) {
+ /* translators: %s: number of folks following the blog */
+ echo wpautop( sprintf( _n( 'Join %s other follower', 'Join %s other followers', $subscribers_total ), number_format_i18n( $subscribers_total ) ) );
+ }
+ ?>
+ <p><input type="text" name="email" style="width: 95%; padding: 1px 2px"
+ placeholder="<?php esc_attr_e( 'Enter your email address' ); ?>" value=""
+ id="subscribe-field<?php if ( Jetpack_Subscriptions_Widget::$instance_count > 1 ) {
+ echo '-' . Jetpack_Subscriptions_Widget::$instance_count;
+ } ?>"/></p>
+ <?php endif; ?>
+
+ <p>
+ <input type="hidden" name="action" value="subscribe"/>
+ <input type="hidden" name="blog_id" value="<?php echo (int) $current_blog->blog_id; ?>"/>
+ <input type="hidden" name="source" value="<?php echo esc_url( $referer ); ?>"/>
+ <input type="hidden" name="sub-type" value="<?php echo esc_attr( $source ); ?>"/>
+ <input type="hidden" name="redirect_fragment" value="<?php echo esc_attr( $widget_id ); ?>"/>
+ <?php wp_nonce_field( 'blogsub_subscribe_' . $current_blog->blog_id, '_wpnonce', false ); ?>
+ <button type="submit"
+ <?php if ( ! empty( $submit_button_classes ) ) { ?>
+ class="<?php echo esc_attr( $submit_button_classes ); ?>"
+ <?php }; ?>
+ <?php if ( ! empty( $submit_button_styles ) ) { ?>
+ style="<?php echo esc_attr( $submit_button_styles ); ?>"
+ <?php }; ?>
+ >
+ <?php
+ echo wp_kses(
+ $subscribe_button,
+ self::$allowed_html_tags_for_submit_button
+ );
+ ?>
+ </button>
+ </p>
+ </form>
+ <?php
+ }
+
+ if ( self::is_jetpack() ) {
+ /**
+ * Filter the subscription form's ID prefix.
+ *
+ * @module subscriptions
+ *
+ * @since 2.7.0
+ *
+ * @param string subscribe-field Subscription form field prefix.
+ * @param int $widget_id Widget ID.
+ */
+ $subscribe_field_id = apply_filters( 'subscribe_field_id', 'subscribe-field', $widget_id );
+ ?>
+ <form action="#" method="post" accept-charset="utf-8" id="subscribe-blog-<?php echo $widget_id; ?>">
+ <?php
+ if ( $subscribe_text && ( ! isset ( $_GET['subscribe'] ) || 'success' != $_GET['subscribe'] ) ) {
+ ?>
+ <div id="subscribe-text"><?php echo wpautop( str_replace( '[total-subscribers]', number_format_i18n( $subscribers_total['value'] ), $subscribe_text ) ); ?></div><?php
+ }
+
+ if ( $show_subscribers_total && 0 < $subscribers_total['value'] ) {
+ /* translators: %s: number of folks following the blog */
+ echo wpautop( sprintf( _n( 'Join %s other subscriber', 'Join %s other subscribers', $subscribers_total['value'], 'jetpack' ), number_format_i18n( $subscribers_total['value'] ) ) );
+ }
+ if ( ! isset ( $_GET['subscribe'] ) || 'success' != $_GET['subscribe'] ) { ?>
+ <p id="subscribe-email">
+ <label id="jetpack-subscribe-label"
+ class="screen-reader-text"
+ for="<?php echo esc_attr( $subscribe_field_id ) . '-' . esc_attr( $widget_id ); ?>">
+ <?php echo ! empty( $subscribe_placeholder ) ? esc_html( $subscribe_placeholder ) : esc_html__( 'Email Address:', 'jetpack' ); ?>
+ </label>
+ <input type="email" name="email" required="required" class="required"
+ value="<?php echo esc_attr( $subscribe_email ); ?>"
+ id="<?php echo esc_attr( $subscribe_field_id ) . '-' . esc_attr( $widget_id ); ?>"
+ placeholder="<?php echo esc_attr( $subscribe_placeholder ); ?>"/>
+ </p>
+
+ <p id="subscribe-submit">
+ <input type="hidden" name="action" value="subscribe"/>
+ <input type="hidden" name="source" value="<?php echo esc_url( $referer ); ?>"/>
+ <input type="hidden" name="sub-type" value="<?php echo esc_attr( $source ); ?>"/>
+ <input type="hidden" name="redirect_fragment" value="<?php echo $widget_id; ?>"/>
+ <?php
+ if ( is_user_logged_in() ) {
+ wp_nonce_field( 'blogsub_subscribe_' . get_current_blog_id(), '_wpnonce', false );
+ }
+ ?>
+ <button type="submit"
+ <?php if ( ! empty( $submit_button_classes ) ) { ?>
+ class="<?php echo esc_attr( $submit_button_classes ); ?>"
+ <?php }; ?>
+ <?php if ( ! empty( $submit_button_styles ) ) { ?>
+ style="<?php echo esc_attr( $submit_button_styles ); ?>"
+ <?php }; ?>
+ name="jetpack_subscriptions_widget"
+ >
+ <?php
+ echo wp_kses(
+ $subscribe_button,
+ self::$allowed_html_tags_for_submit_button
+ ); ?>
+ </button>
+ </p>
+ <?php } ?>
+ </form>
+ <?php }
+ }
+
+ /**
+ * Determines if the current user is subscribed to the blog.
+ *
+ * @return bool Is the person already subscribed.
+ */
+ static function is_current_user_subscribed() {
+ $subscribed = isset( $_GET['subscribe'] ) && 'success' == $_GET['subscribe'];
+
+ if ( self::is_wpcom() && class_exists( 'Blog_Subscription' ) && class_exists( 'Blog_Subscriber' ) ) {
+ $subscribed = is_user_logged_in() && Blog_Subscription::is_subscribed( new Blog_Subscriber() );
+ }
+
+ return $subscribed;
+ }
+
+ /**
+ * Is this script running in the wordpress.com environment?
+ *
+ * @return bool
+ */
+ static function is_wpcom() {
+ return defined( 'IS_WPCOM' ) && IS_WPCOM;
+ }
+
+ /**
+ * Is this script running in a self-hosted environment?
+ *
+ * @return bool
+ */
+ static function is_jetpack() {
+ return ! self::is_wpcom();
+ }
+
+ /**
+ * Used to determine if there is a valid status slug within the wordpress.com environment.
+ *
+ * @return bool
+ */
+ static function wpcom_has_status_message() {
+ return isset( $_GET['blogsub'] ) &&
+ in_array(
+ $_GET['blogsub'],
+ array(
+ 'confirming',
+ 'blocked',
+ 'flooded',
+ 'spammed',
+ 'subscribed',
+ 'pending',
+ 'confirmed',
+ )
+ );
+ }
+
+ /**
+ * Determine the amount of folks currently subscribed to the blog.
+ *
+ * @return int|array
+ */
+ static function fetch_subscriber_count() {
+ $subs_count = 0;
+
+ if ( self::is_jetpack() ) {
+ $subs_count = get_transient( 'wpcom_subscribers_total' );
+ if ( false === $subs_count || 'failed' == $subs_count['status'] ) {
+ Jetpack::load_xml_rpc_client();
+
+ $xml = new Jetpack_IXR_Client( array( 'user_id' => JETPACK_MASTER_USER, ) );
+
+ $xml->query( 'jetpack.fetchSubscriberCount' );
+
+ if ( $xml->isError() ) { // if we get an error from .com, set the status to failed so that we will try again next time the data is requested
+ $subs_count = array(
+ 'status' => 'failed',
+ 'code' => $xml->getErrorCode(),
+ 'message' => $xml->getErrorMessage(),
+ 'value' => ( isset( $subs_count['value'] ) ) ? $subs_count['value'] : 0,
+ );
+ } else {
+ $subs_count = array(
+ 'status' => 'success',
+ 'value' => $xml->getResponse(),
+ );
+ }
+
+ set_transient( 'wpcom_subscribers_total', $subs_count, 3600 ); // try to cache the result for at least 1 hour
+ }
+ }
+
+ if ( self::is_wpcom() && function_exists( 'wpcom_reach_total_for_blog' ) ) {
+ $subs_count = wpcom_reach_total_for_blog();
+ }
+
+ return $subs_count;
+ }
+
+ /**
+ * Updates a particular instance of a widget when someone saves it in wp-admin.
+ *
+ * @param array $new_instance
+ * @param array $old_instance
+ *
+ * @return array
+ */
+ function update( $new_instance, $old_instance ) {
+ $instance = $old_instance;
+
+ if ( self::is_jetpack() ) {
+ $instance['title'] = wp_kses( stripslashes( $new_instance['title'] ), array() );
+ $instance['subscribe_placeholder'] = wp_kses( stripslashes( $new_instance['subscribe_placeholder'] ), array() );
+ $instance['subscribe_button'] = wp_kses( stripslashes( $new_instance['subscribe_button'] ), array() );
+ $instance['success_message'] = wp_kses( stripslashes( $new_instance['success_message'] ), array() );
+ }
+
+ if ( self::is_wpcom() ) {
+ $instance['title'] = strip_tags( stripslashes( $new_instance['title'] ) );
+ $instance['title_following'] = strip_tags( stripslashes( $new_instance['title_following'] ) );
+ $instance['subscribe_logged_in'] = wp_filter_post_kses( stripslashes( $new_instance['subscribe_logged_in'] ) );
+ $instance['subscribe_button'] = strip_tags( stripslashes( $new_instance['subscribe_button'] ) );
+ }
+
+ $instance['show_subscribers_total'] = isset( $new_instance['show_subscribers_total'] ) && $new_instance['show_subscribers_total'];
+ $instance['show_only_email_and_button'] = isset( $new_instance['show_only_email_and_button'] ) && $new_instance['show_only_email_and_button'];
+ $instance['subscribe_text'] = wp_filter_post_kses( stripslashes( $new_instance['subscribe_text'] ) );
+
+ return $instance;
+ }
+
+ /**
+ * The default args for rendering a subscription form.
+ *
+ * @return array
+ */
+ static function defaults() {
+ $defaults = array(
+ 'show_subscribers_total' => true,
+ 'show_only_email_and_button' => false
+ );
+
+ if ( self::is_jetpack() ) {
+ $defaults['title'] = esc_html__( 'Subscribe to Blog via Email', 'jetpack' );
+ $defaults['subscribe_text'] = esc_html__( 'Enter your email address to subscribe to this blog and receive notifications of new posts by email.', 'jetpack' );
+ $defaults['subscribe_placeholder'] = esc_html__( 'Email Address', 'jetpack' );
+ $defaults['subscribe_button'] = esc_html__( 'Subscribe', 'jetpack' );
+ $defaults['success_message'] = esc_html__( "Success! An email was just sent to confirm your subscription. Please find the email now and click 'Confirm Follow' to start subscribing.", 'jetpack' );
+ }
+
+ if ( self::is_wpcom() ) {
+ $defaults['title'] = __( 'Follow Blog via Email' );
+ $defaults['title_following'] = __( 'You are following this blog' );
+ $defaults['subscribe_text'] = __( 'Enter your email address to follow this blog and receive notifications of new posts by email.' );
+ $defaults['subscribe_button'] = __( 'Follow' );
+ $defaults['subscribe_logged_in'] = __( 'Click to follow this blog and receive notifications of new posts by email.' );
+ }
+
+ return $defaults;
+ }
+
+ /**
+ * Renders the widget's options form in wp-admin.
+ *
+ * @param array $instance
+ */
+ function form( $instance ) {
+ $instance = wp_parse_args( (array) $instance, $this->defaults() );
+ $show_subscribers_total = checked( $instance['show_subscribers_total'], true, false );
+
+
+ if ( self::is_wpcom() ) {
+ $title = esc_attr( stripslashes( $instance['title'] ) );
+ $title_following = esc_attr( stripslashes( $instance['title_following'] ) );
+ $subscribe_text = esc_attr( stripslashes( $instance['subscribe_text'] ) );
+ $subscribe_logged_in = esc_attr( stripslashes( $instance['subscribe_logged_in'] ) );
+ $subscribe_button = esc_attr( stripslashes( $instance['subscribe_button'] ) );
+ $subscribers_total = self::fetch_subscriber_count();
+ }
+
+ if ( self::is_jetpack() ) {
+ $title = stripslashes( $instance['title'] );
+ $subscribe_text = stripslashes( $instance['subscribe_text'] );
+ $subscribe_placeholder = stripslashes( $instance['subscribe_placeholder'] );
+ $subscribe_button = stripslashes( $instance['subscribe_button'] );
+ $success_message = stripslashes( $instance['success_message'] );
+ $subs_fetch = self::fetch_subscriber_count();
+ if ( 'failed' == $subs_fetch['status'] ) {
+ printf( '<div class="error inline"><p>%s: %s</p></div>', esc_html( $subs_fetch['code'] ), esc_html( $subs_fetch['message'] ) );
+ }
+ $subscribers_total = number_format_i18n( $subs_fetch['value'] );
+ }
+
+ if ( self::is_wpcom() ) : ?>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>">
+ <?php _e( 'Widget title for non-followers:' ); ?>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>"
+ name="<?php echo $this->get_field_name( 'title' ); ?>" type="text"
+ value="<?php echo $title; ?>"/>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title_following' ); ?>">
+ <?php _e( 'Widget title for followers:' ); ?>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'title_following' ); ?>"
+ name="<?php echo $this->get_field_name( 'title_following' ); ?>" type="text"
+ value="<?php echo $title_following; ?>"/>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'subscribe_logged_in' ); ?>">
+ <?php _e( 'Optional text to display to logged in WordPress.com users:' ); ?>
+ <textarea style="width: 95%" id="<?php echo $this->get_field_id( 'subscribe_logged_in' ); ?>"
+ name="<?php echo $this->get_field_name( 'subscribe_logged_in' ); ?>"
+ type="text"><?php echo $subscribe_logged_in; ?></textarea>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'subscribe_text' ); ?>">
+ <?php _e( 'Optional text to display to non-WordPress.com users:' ); ?>
+ <textarea style="width: 95%" id="<?php echo $this->get_field_id( 'subscribe_text' ); ?>"
+ name="<?php echo $this->get_field_name( 'subscribe_text' ); ?>"
+ type="text"><?php echo $subscribe_text; ?></textarea>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'subscribe_button' ); ?>">
+ <?php _e( 'Follow Button Text:' ); ?>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'subscribe_button' ); ?>"
+ name="<?php echo $this->get_field_name( 'subscribe_button' ); ?>" type="text"
+ value="<?php echo $subscribe_button; ?>"/>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'show_subscribers_total' ); ?>">
+ <input type="checkbox" id="<?php echo $this->get_field_id( 'show_subscribers_total' ); ?>"
+ name="<?php echo $this->get_field_name( 'show_subscribers_total' ); ?>"
+ value="1"<?php echo $show_subscribers_total; ?> />
+ <?php echo esc_html( sprintf( _n( 'Show total number of followers? (%s follower)', 'Show total number of followers? (%s followers)', $subscribers_total ), number_format_i18n( $subscribers_total ) ) ); ?>
+ </label>
+ </p>
+ <?php endif;
+
+ if ( self::is_jetpack() ) : ?>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>">
+ <?php _e( 'Widget title:', 'jetpack' ); ?>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>"
+ name="<?php echo $this->get_field_name( 'title' ); ?>" type="text"
+ value="<?php echo esc_attr( $title ); ?>"/>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'subscribe_text' ); ?>">
+ <?php _e( 'Optional text to display to your readers:', 'jetpack' ); ?>
+ <textarea class="widefat" id="<?php echo $this->get_field_id( 'subscribe_text' ); ?>"
+ name="<?php echo $this->get_field_name( 'subscribe_text' ); ?>"
+ rows="3"><?php echo esc_html( $subscribe_text ); ?></textarea>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'subscribe_placeholder' ); ?>">
+ <?php esc_html_e( 'Subscribe Placeholder:', 'jetpack' ); ?>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'subscribe_placeholder' ); ?>"
+ name="<?php echo $this->get_field_name( 'subscribe_placeholder' ); ?>" type="text"
+ value="<?php echo esc_attr( $subscribe_placeholder ); ?>"/>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'subscribe_button' ); ?>">
+ <?php _e( 'Subscribe Button:', 'jetpack' ); ?>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'subscribe_button' ); ?>"
+ name="<?php echo $this->get_field_name( 'subscribe_button' ); ?>" type="text"
+ value="<?php echo esc_attr( $subscribe_button ); ?>"/>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'success_message' ); ?>">
+ <?php _e( 'Success Message Text:', 'jetpack' ); ?>
+ <textarea class="widefat" id="<?php echo $this->get_field_id( 'success_message' ); ?>"
+ name="<?php echo $this->get_field_name( 'success_message' ); ?>"
+ rows="5"><?php echo esc_html( $success_message ); ?></textarea>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'show_subscribers_total' ); ?>">
+ <input type="checkbox" id="<?php echo $this->get_field_id( 'show_subscribers_total' ); ?>"
+ name="<?php echo $this->get_field_name( 'show_subscribers_total' ); ?>"
+ value="1"<?php echo $show_subscribers_total; ?> />
+ <?php echo esc_html( sprintf( _n( 'Show total number of subscribers? (%s subscriber)', 'Show total number of subscribers? (%s subscribers)', $subscribers_total, 'jetpack' ), $subscribers_total ) ); ?>
+ </label>
+ </p>
+ <?php endif;
+ }
+}
+
+if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'class_alias' ) ) {
+ class_alias( 'Jetpack_Subscriptions_Widget', 'Blog_Subscription_Widget' );
+}
+
+function get_jetpack_blog_subscriptions_widget_classname() {
+ return ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ?
+ 'Blog_Subscription_Widget' :
+ 'Jetpack_Subscriptions_Widget';
+}
+
+function jetpack_do_subscription_form( $instance ) {
+ if ( empty( $instance ) || ! is_array( $instance ) ) {
+ $instance = array();
+ }
+
+ if ( empty( $instance['show_subscribers_total'] ) || 'false' === $instance['show_subscribers_total'] ) {
+ $instance['show_subscribers_total'] = false;
+ } else {
+ $instance['show_subscribers_total'] = true;
+ }
+
+ $show_only_email_and_button = isset( $instance['show_only_email_and_button'] ) ? $instance['show_only_email_and_button'] : false;
+ $submit_button_text = isset( $instance['submit_button_text'] ) ? $instance['submit_button_text'] : '';
+
+
+
+ // Build up a string with the submit button's classes and styles and set it on the instance
+ $submit_button_classes = isset( $instance['submit_button_classes'] ) ? $instance['submit_button_classes'] : '';
+ $submit_button_styles = '';
+ if ( isset( $instance['custom_background_button_color'] ) ) {
+ $submit_button_styles .= 'background-color: ' . $instance['custom_background_button_color'] . '; ';
+ }
+ if ( isset( $instance['custom_text_button_color'] ) ) {
+ $submit_button_styles .= 'color: ' . $instance['custom_text_button_color'] . ';';
+ }
+
+ $instance = shortcode_atts(
+ Jetpack_Subscriptions_Widget::defaults(),
+ $instance,
+ 'jetpack_subscription_form'
+ );
+
+ // These must come after the call to shortcode_atts()
+ $instance['submit_button_text'] = $submit_button_text;
+ $instance['show_only_email_and_button'] = $show_only_email_and_button;
+ if ( ! empty( $submit_button_classes ) ) {
+ $instance['submit_button_classes'] = $submit_button_classes;
+ }
+ if ( ! empty ( $submit_button_styles ) ) {
+ $instance['submit_button_styles'] = $submit_button_styles;
+ }
+
+ $args = array(
+ 'before_widget' => '<div class="jetpack_subscription_widget">',
+ );
+ ob_start();
+ the_widget( get_jetpack_blog_subscriptions_widget_classname(), $instance, $args );
+ $output = ob_get_clean();
+
+ return $output;
+}
+
+add_shortcode( 'jetpack_subscription_form', 'jetpack_do_subscription_form' );
+add_shortcode( 'blog_subscription_form', 'jetpack_do_subscription_form' );
+
+function jetpack_blog_subscriptions_init() {
+ register_widget( get_jetpack_blog_subscriptions_widget_classname() );
+}
+
+add_action( 'widgets_init', 'jetpack_blog_subscriptions_init' );
+
+function jetpack_register_subscriptions_block() {
+ if ( class_exists( 'WP_Block_Type_Registry' ) && ! WP_Block_Type_Registry::get_instance()->is_registered( 'jetpack/subscriptions' ) ) {
+ jetpack_register_block( 'jetpack/subscriptions' );
+ }
+}
+
+add_action( 'init', 'jetpack_register_subscriptions_block' );
diff --git a/plugins/jetpack/modules/theme-tools.php b/plugins/jetpack/modules/theme-tools.php
new file mode 100644
index 00000000..afb8b000
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools.php
@@ -0,0 +1,70 @@
+<?php
+/*
+ * Load code specific to themes or theme tools
+ * This file is special, and is not an actual `module` as such.
+ * It is included by ./module-extras.php
+ */
+
+function jetpack_load_theme_tools() {
+ if ( current_theme_supports( 'tonesque' ) ) {
+ jetpack_require_lib( 'tonesque' );
+ }
+}
+add_action( 'init', 'jetpack_load_theme_tools', 30 );
+
+/**
+ * Load theme compat file if it exists.
+ */
+function jetpack_load_theme_compat() {
+
+ /**
+ * Filter theme compat files.
+ *
+ * Themes can add their own compat files here if they like. For example:
+ *
+ * add_filter( 'jetpack_theme_compat_files', 'mytheme_jetpack_compat_file' );
+ * function mytheme_jetpack_compat_file( $files ) {
+ * $files['mytheme'] = locate_template( 'jetpack-compat.php' );
+ * return $files;
+ * }
+ *
+ * @module theme-tools
+ *
+ * @since 2.8.0
+ *
+ * @param array Associative array of theme compat files to load.
+ */
+ $compat_files = apply_filters( 'jetpack_theme_compat_files', array(
+ 'twentyfourteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentyfourteen.php',
+ 'twentyfifteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentyfifteen.php',
+ 'twentysixteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentysixteen.php',
+ 'twentyseventeen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentyseventeen.php',
+ 'twentynineteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentynineteen.php',
+ ) );
+
+ _jetpack_require_compat_file( get_stylesheet(), $compat_files );
+
+ if ( is_child_theme() ) {
+ _jetpack_require_compat_file( get_template(), $compat_files );
+ }
+}
+add_action( 'after_setup_theme', 'jetpack_load_theme_compat', -1 );
+
+
+/**
+ * Requires a file once, if the passed key exists in the files array.
+ *
+ * @access private
+ * @param string $key
+ * @param array $files
+ * @return void
+ */
+function _jetpack_require_compat_file( $key, $files ) {
+ if ( ! is_string( $key ) ) {
+ return new WP_Error( 'key_not_string', 'The specified key is not actually a string.', compact( 'key' ) );
+ }
+
+ if ( array_key_exists( $key, $files ) && is_readable( $files[ $key ] ) ) {
+ require_once $files[ $key ];
+ }
+}
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentyfifteen-rtl.css b/plugins/jetpack/modules/theme-tools/compat/twentyfifteen-rtl.css
new file mode 100644
index 00000000..7557cfd5
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentyfifteen-rtl.css
@@ -0,0 +1,744 @@
+/**
+ * Jetpack Portfolio Shortcode
+ */
+
+.site .portfolio-entry {
+ margin-bottom: 1.6em;
+}
+
+.site .portfolio-entry-title,
+.site .portfolio-entry-meta,
+.site .portfolio-entry-content {
+ font-size: 1.2rem;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.site .portfolio-featured-image + .portfolio-entry-title {
+ margin-top: 0.75em;
+ margin-bottom: 0.75em;
+}
+
+.site .portfolio-entry-meta {
+ font-family: "Noto Sans", sans-serif;
+}
+
+.site .portfolio-entry-content .more-link:after {
+ font-size: 16px;
+ top: 1px;
+}
+
+/**
+ * Jetpack Widgets
+ */
+
+/* Blog Subscriptions Widget */
+.jetpack_subscription_widget #subscribe-email input {
+ padding: 0.375em;
+ width: 100%;
+}
+
+.jetpack_subscription_widget form > :last-child {
+ margin-bottom: 0;
+}
+
+/* Display WordPress Posts Widget */
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts {
+ margin: 0;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4 {
+ font-size: inherit;
+ margin: 0 0 1.6em;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts img {
+ margin: 0 0 1.6em;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ font-size: inherit;
+ line-height: 1.6 !important;
+ margin: 0 0 1.6em !important;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts > :last-child {
+ margin-bottom: 0 !important;
+}
+
+/* Gallery Widget */
+.widget-gallery .slideshow-window {
+ border-radius: 0;
+}
+
+/* Gravatar Profile Widget */
+.widget-area .widget-grofile .grofile-thumbnail {
+ border-radius: 50%;
+ max-width: 200px;
+}
+
+.widget-area .widget-grofile h4 {
+ margin: 1.6em 0 0;
+}
+
+.widget-area .widget-grofile .grofile-accounts {
+ margin-top: 0.8em;
+}
+
+/* Image Widget */
+.widget_image .wp-caption {
+ margin-bottom: 0;
+}
+
+.widget_image .wp-caption-text {
+ padding-bottom: 0;
+}
+
+/* RSS Links Widget */
+.widget_rss_links img {
+ position: relative;
+ top: -2px;
+}
+
+
+/* List type widgets */
+.widget_rss_links ul,
+.widget_top-posts ul ,
+.widget_upcoming_events_widget ul {
+ list-style: none;
+ margin: 0;
+}
+
+.widget_rss_links li,
+.widget_top-posts li,
+.widget_upcoming_events_widget li {
+ border-top: 1px solid #eaeaea;
+ border-top: 1px solid rgba(51, 51, 51, 0.1);
+ padding: 0.7667em 0;
+}
+
+.widget_rss_links li:first-child,
+.widget_top-posts li:first-child,
+.widget_upcoming_events_widget li:first-child {
+ border-top: 0;
+ padding-top: 0;
+}
+
+.widget_rss_links li:last-child,
+.widget_top-posts li:last-child,
+.widget_upcoming_events_widget li:last-child {
+ padding-bottom: 0;
+}
+
+
+/**
+ * Shortcodes Embeds
+ */
+
+/* Facebook */
+.fb_iframe_widget {
+ margin-bottom: 1.6em;
+ max-width: 100%;
+}
+
+.fb_iframe_widget span {
+ max-width: 100%;
+}
+
+/* Gist */
+.gist table {
+ table-layout: auto;
+}
+
+.site .gist .gist-file {
+ margin-bottom: 1.5em;
+}
+
+/* Googlemaps */
+.googlemaps {
+ margin-bottom: 1.6em;
+}
+
+.googlemaps iframe {
+ margin-bottom: 0;
+}
+
+/* Crowdsignal */
+.PDS_Poll,
+.CSS_Poll {
+ display: block !important;
+ margin-bottom: 1.6em;
+}
+
+.PDS_Poll .pds-box,
+.CSS_Poll .css-box {
+ max-width: 100%;
+ width: auto;
+}
+
+/* Presentation */
+.site .presentation-wrapper {
+ margin: 0 auto 1.6em;
+}
+
+/* Recipes */
+.site .jetpack-recipe {
+ border: 0;
+ margin: 0 0 1.6em;
+ padding: 0;
+}
+
+.site .jetpack-recipe-title {
+ border: 0;
+ margin-top: 0;
+ padding: 0;
+}
+
+.site .jetpack-recipe .jetpack-recipe-meta {
+ font-size: inherit;
+ margin: 0;
+}
+
+/* Slideshow */
+.site .slideshow-window {
+ border-radius: 0;
+ margin-bottom: 1.6em;
+}
+
+/* Twitter-timeline */
+iframe[id*="twitter-widget-"] {
+ display: block;
+}
+
+/* Vine */
+.vine-embed {
+ display: block;
+}
+
+/* VideoPress */
+.site .video-player {
+ margin-bottom: 1.6em;
+ padding: 0;
+}
+
+.video-player object {
+ margin-bottom: 0;
+}
+
+
+/**
+ * Tiled gallery
+ */
+
+.site .tiled-gallery {
+ margin-bottom: 1.6em;
+}
+
+
+/**
+ * Jetpack Comments
+ */
+
+.comment-form iframe {
+ margin: 0;
+}
+
+.comment-form .subscribe-label {
+ font-weight: 400;
+ text-transform: none;
+}
+
+.comment-subscription-form {
+ font-size: 12px;
+ font-size: 1.2rem;
+ line-height: 1.5em;
+ margin: 2em 0 0;
+}
+
+.comment-subscription-form + .comment-subscription-form {
+ margin-top: 1em;
+}
+
+
+/**
+ * Sharing
+ */
+
+.hentry div.sharedaddy h3.sd-title,
+.hentry h3.sd-title {
+ font-family: "Noto Sans", sans-serif;
+ font-size: 12px;
+ font-size: 1.2rem;
+ line-height: 1;
+ margin: 0 0 1em 0;
+ opacity: 0.7;
+ text-transform: uppercase;
+}
+
+
+/**
+ * Related Posts
+ */
+
+.hentry #jp-relatedposts {
+ margin: 0;
+ padding-top: 0;
+}
+
+.hentry .jp-relatedposts-post-title a {
+ border-bottom: 0;
+}
+
+.hentry .jp-relatedposts-headline em:before {
+ opacity: 0.7;
+}
+
+.hentry div#jp-relatedposts h3.jp-relatedposts-headline {
+ font-family: "Noto Sans", sans-serif;
+ font-size: 12px;
+ font-size: 1.2rem;
+ opacity: 0.7;
+ text-transform: uppercase;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items-visual {
+ margin-left: 0;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post {
+ margin-bottom: 1.6em;
+ opacity: 1;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+.hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 1.2rem;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.hentry .jp-relatedposts-post-title {
+ font-weight: 700;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a {
+ font-weight: inherit;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items div.jp-relatedposts-post:hover .jp-relatedposts-post-title a,
+.hentry div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a {
+ text-decoration: none;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-context,
+.hentry div#jp-relatedposts div.jp-relatedposts-items-visual div.jp-relatedposts-post-nothumbs p.jp-relatedposts-post-excerpt {
+ opacity: 0.7;
+}
+
+.hentry .jp-relatedposts-post-img {
+ margin-bottom: 0.6em;
+}
+
+
+/**
+ * Stats
+ */
+
+#wpstats {
+ display: none;
+}
+
+
+/**
+ * Media Queries
+ */
+
+@media screen and (min-width: 46.25em) {
+ .site .portfolio-entry {
+ margin-bottom: 1.6471em;
+ }
+
+ .site .portfolio-entry-title,
+ .site .portfolio-entry-meta,
+ .site .portfolio-entry-content {
+ font-size: 1.4rem;
+ font-size: 14px;
+ }
+
+ .site .portfolio-entry-content .more-link:after {
+ top: 3px;
+ }
+
+ .site .hentry .gist .gist-file {
+ margin-bottom: 1.75em;
+ }
+
+ .site .widget .gist .gist-file {
+ margin-bottom: 1.3125em;
+ }
+
+ .hentry .fb_iframe_widget,
+ .hentry .googlemaps,
+ .hentry .PDS_Poll,
+ .hentry .CSS_Poll,
+ .site .hentry .presentation-wrapper,
+ .site .hentry .jetpack-recipe,
+ .site .hentry .slideshow-window,
+ .site .hentry .video-player {
+ margin-bottom: 1.6471em;
+ }
+
+ .widget .fb_iframe_widget,
+ .widget .googlemaps,
+ .widget .PDS_Poll,
+ .widget .CSS_Poll,
+ .site .widget .presentation-wrapper,
+ .site .widget .jetpack-recipe,
+ .site .widget .slideshow-window,
+ .site .widget .video-player {
+ margin-bottom: 1.5em;
+ }
+
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4,
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts img {
+ margin: 0 0 1.5em;
+ }
+
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ line-height: 1.5 !important;
+ margin: 0 0 1.5em !important;
+ }
+
+ .widget-area .widget-grofile h4 {
+ margin: 1.5em 0 0;
+ }
+
+ .widget-area .widget-grofile .grofile-accounts {
+ margin-top: 0.75em;
+ }
+
+ .jetpack_subscription_widget #subscribe-email input {
+ padding: 0.5625em;
+ }
+
+ .widget_rss_links li,
+ .widget_top-posts li,
+ .widget_upcoming_events_widget li {
+ padding: 0.9643em 0;
+ }
+
+ .site .tiled-gallery {
+ margin-bottom: 1.6471em;
+ }
+
+ .jetpack-video-wrapper {
+ margin-bottom: 1.6471em;
+ }
+
+ .comment-subscription-form {
+ font-size: 14px;
+ font-size: 1.4rem;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post {
+ margin-bottom: 0;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+ .hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 1.4rem;
+ font-size: 14px;
+ }
+
+ .jp-relatedposts-post-img {
+ margin-bottom: 0.6176em;
+ }
+}
+
+@media screen and (min-width: 55em) {
+ .site .portfolio-entry {
+ margin-bottom: 1.6842em
+ }
+
+ .site .portfolio-entry-title,
+ .site .portfolio-entry-meta,
+ .site .portfolio-entry-content {
+ font-size: 1.6rem;
+ font-size: 16px;
+ }
+
+ .site .portfolio-entry-content .more-link:after {
+ font-size: 24px;
+ top: 0;
+ }
+
+ .site .hentry .gist .gist-file {
+ margin-bottom: 2em;
+ }
+
+ .site .widget .gist .gist-file {
+ margin-bottom: 1.5em;
+ }
+
+ .hentry .fb_iframe_widget,
+ .hentry .googlemaps,
+ .hentry .PDS_Poll,
+ .hentry .CSS_Poll,
+ .site .hentry .presentation-wrapper,
+ .site .hentry .jetpack-recipe,
+ .site .hentry .slideshow-window,
+ .site .hentry .video-player {
+ margin-bottom: 1.6842em;
+ }
+
+ .site .tiled-gallery {
+ margin-bottom: 1.6842em;
+ }
+
+ .jetpack_subscription_widget #subscribe-email input {
+ padding: 0.75em;
+ }
+
+ .widget_rss_links li,
+ .widget_top-posts li,
+ .widget_upcoming_events_widget li {
+ padding: 0.7188em 0;
+ }
+
+ .jetpack-video-wrapper {
+ margin-bottom: 1.6842em;
+ }
+
+ .comment-subscription-form {
+ font-size: 16px;
+ font-size: 1.6rem;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+ .hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 1.6rem;
+ font-size: 16px;
+ line-height: 1.5em;
+ }
+
+ .jp-relatedposts-post-img {
+ margin-bottom: 0.6315em;
+ }
+}
+
+@media screen and (min-width: 59.6875em) {
+ .site .portfolio-entry {
+ margin-bottom: 1.6em
+ }
+
+ .site .portfolio-entry-title,
+ .site .portfolio-entry-meta,
+ .site .portfolio-entry-content {
+ font-size: 1.2rem;
+ font-size: 12px;
+ }
+
+ .site .portfolio-entry-content .more-link:after {
+ font-size: 16px;
+ top: 1px;
+ }
+
+ .site .hentry .gist .gist-file {
+ margin-bottom: 1.5em;
+ }
+
+ .site .widget .gist .gist-file {
+ margin-bottom: 1.125em;
+ }
+
+ .hentry .fb_iframe_widget,
+ .hentry .googlemaps,
+ .hentry .PDS_Poll,
+ .hentry .CSS_Poll,
+ .site .hentry .presentation-wrapper,
+ .site .hentry .jetpack-recipe,
+ .site .hentry .slideshow-window,
+ .site .hentry .video-player {
+ margin-bottom: 1.6em;
+ }
+
+ .site .tiled-gallery {
+ margin-bottom: 1.6em;
+ }
+
+ .jetpack_subscription_widget #subscribe-email input {
+ padding: 0.4583em;
+ }
+
+ .jetpack-video-wrapper {
+ margin-bottom: 1.6em;
+ }
+
+ .widget-area .widget-grofile .grofile-thumbnail {
+ max-width: 100%;
+ }
+
+ .widget_rss_links li,
+ .widget_top-posts li,
+ .widget_upcoming_events_widget li {
+ padding: 0.4583em 0;
+ }
+
+ .comment-subscription-form {
+ font-size: 12px;
+ font-size: 1.2rem;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+ .hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 1.2rem;
+ font-size: 12px;
+ line-height: 1.5;
+ }
+
+ .jp-relatedposts-post-img {
+ margin-bottom: 0.6em;
+ }
+}
+
+@media screen and (min-width: 68.75em) {
+ .site .portfolio-entry {
+ margin-bottom: 1.6471em
+ }
+
+ .site .portfolio-entry-title,
+ .site .portfolio-entry-meta,
+ .site .portfolio-entry-content {
+ font-size: 1.4rem;
+ font-size: 14px;
+ }
+
+ .site .portfolio-entry-content .more-link:after {
+ top: 3px;
+ }
+
+ .site .hentry .gist .gist-file {
+ margin-bottom: 1.75em;
+ }
+
+ .site .widget .gist .gist-file {
+ margin-bottom: 1.3125em;
+ }
+
+ .hentry .fb_iframe_widget,
+ .hentry .googlemaps,
+ .hentry .PDS_Poll,
+ .hentry .CSS_Poll,
+ .site .hentry .presentation-wrapper,
+ .site .hentry .jetpack-recipe,
+ .site .hentry .slideshow-window,
+ .site .hentry .video-player {
+ margin-bottom: 1.6471em;
+ }
+
+ .site .tiled-gallery {
+ margin-bottom: 1.6471em;
+ }
+
+ .jetpack_subscription_widget #subscribe-email input {
+ padding: 0.5em;
+ }
+
+ .widget_rss_links li,
+ .widget_top-posts li,
+ .widget_upcoming_events_widget li {
+ padding: 0.4643em 0;
+ }
+
+ .jetpack-video-wrapper {
+ margin-bottom: 1.6471em;
+ }
+
+ .comment-subscription-form {
+ font-size: 14px;
+ font-size: 1.4rem;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+ .hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 14px;
+ font-size: 1.4rem;
+ }
+
+ .jp-relatedposts-post-img {
+ margin-bottom: 0.6176em;
+ }
+}
+
+@media screen and (min-width: 77.5em) {
+ .site .portfolio-entry {
+ margin-bottom: 1.6842em
+ }
+
+ .site .portfolio-entry-title,
+ .site .portfolio-entry-meta,
+ .site .portfolio-entry-content {
+ font-size: 1.6rem;
+ font-size: 16px;
+ }
+
+ .site .portfolio-entry-content .more-link:after {
+ font-size: 24px;
+ top: 0;
+ }
+
+ .site .hentry .gist .gist-file {
+ margin-bottom: 2em;
+ }
+
+ .site .widget .gist .gist-file {
+ margin-bottom: 1.5em;
+ }
+
+ .hentry .fb_iframe_widget,
+ .hentry .googlemaps,
+ .hentry .PDS_Poll,
+ .hentry .CSS_Poll,
+ .site .hentry .presentation-wrapper,
+ .site .hentry .jetpack-recipe,
+ .site .hentry .slideshow-window,
+ .site .hentry .video-player {
+ margin-bottom: 1.6842em;
+ }
+
+ .widget_rss_links li,
+ .widget_top-posts li,
+ .widget_upcoming_events_widget li {
+ padding: 0.4688em 0;
+ }
+
+ .site .tiled-gallery {
+ margin-bottom: 1.6842em;
+ }
+
+ .jetpack-video-wrapper {
+ margin-bottom: 1.6842em;
+ }
+
+ .hentry div.sharedaddy h3.sd-title,
+ .hentry div#jp-relatedposts h3.jp-relatedposts-headline {
+ font-size: 13px;
+ font-size: 1.3rem;
+ }
+
+ .comment-subscription-form {
+ font-size: 16px;
+ font-size: 1.6rem;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+ .hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 16px;
+ font-size: 1.6rem;
+ line-height: 1.5em;
+ }
+
+ .jp-relatedposts-post-img {
+ margin-bottom: 0.6315em;
+ }
+}
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentyfifteen.css b/plugins/jetpack/modules/theme-tools/compat/twentyfifteen.css
new file mode 100644
index 00000000..cd343681
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentyfifteen.css
@@ -0,0 +1,769 @@
+/**
+ * Jetpack Portfolio Shortcode
+ */
+
+.site .portfolio-entry {
+ margin-bottom: 1.6em;
+}
+
+.site .portfolio-entry-title,
+.site .portfolio-entry-meta,
+.site .portfolio-entry-content {
+ font-size: 1.2rem;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.site .portfolio-featured-image + .portfolio-entry-title {
+ margin-top: 0.75em;
+ margin-bottom: 0.75em;
+}
+
+.site .portfolio-entry-meta {
+ font-family: "Noto Sans", sans-serif;
+}
+
+.site .portfolio-entry-content .more-link:after {
+ font-size: 16px;
+ top: 1px;
+}
+
+/**
+ * Jetpack Widgets
+ */
+
+/* Blog Subscriptions Widget */
+.jetpack_subscription_widget #subscribe-email input {
+ padding: 0.375em;
+ width: 100%;
+}
+
+.jetpack_subscription_widget form > :last-child {
+ margin-bottom: 0;
+}
+
+/* Display WordPress Posts Widget */
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts {
+ margin: 0;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4 {
+ font-size: inherit;
+ margin: 0 0 1.6em;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts img {
+ margin: 0 0 1.6em;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ font-size: inherit;
+ line-height: 1.6 !important;
+ margin: 0 0 1.6em !important;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts > :last-child {
+ margin-bottom: 0 !important;
+}
+
+/* Gallery Widget */
+.widget-gallery .slideshow-window {
+ border-radius: 0;
+}
+
+/* Gravatar Profile Widget */
+.widget-area .widget-grofile .grofile-thumbnail {
+ border-radius: 50%;
+ max-width: 200px;
+}
+
+.widget-area .widget-grofile h4 {
+ margin: 1.6em 0 0;
+}
+
+.widget-area .widget-grofile .grofile-accounts {
+ margin-top: 0.8em;
+}
+
+/* Image Widget */
+.widget_image .wp-caption {
+ margin-bottom: 0;
+}
+
+.widget_image .wp-caption-text {
+ padding-bottom: 0;
+}
+
+/* RSS Links Widget */
+.widget_rss_links img {
+ position: relative;
+ top: -2px;
+}
+
+
+/* List type widgets */
+.widget_rss_links ul,
+.widget_top-posts ul ,
+.widget_upcoming_events_widget ul {
+ list-style: none;
+ margin: 0;
+}
+
+.widget_rss_links li,
+.widget_top-posts li,
+.widget_upcoming_events_widget li {
+ border-top: 1px solid #eaeaea;
+ border-top: 1px solid rgba(51, 51, 51, 0.1);
+ padding: 0.7667em 0;
+}
+
+.widget_rss_links li:first-child,
+.widget_top-posts li:first-child,
+.widget_upcoming_events_widget li:first-child {
+ border-top: 0;
+ padding-top: 0;
+}
+
+.widget_rss_links li:last-child,
+.widget_top-posts li:last-child,
+.widget_upcoming_events_widget li:last-child {
+ padding-bottom: 0;
+}
+
+/* Authors Widget */
+.widget_authors > ul > li > a {
+ margin-bottom: 1em;
+ display: inline-block;
+}
+
+.widget_authors ul {
+ list-style: none;
+ margin: 0;
+}
+
+.widget_authors li {
+ border-top: 1px solid #eaeaea;
+ border-top: 1px solid rgba(51, 51, 51, 0.1);
+ padding: 0.7667em 0;
+}
+
+.widget_authors li:first-child {
+ border-top: 0;
+ padding-top: 0;
+}
+
+.widget_authors li:last-child {
+ padding-bottom: 0;
+}
+
+/**
+ * Shortcodes Embeds
+ */
+
+/* Facebook */
+.fb_iframe_widget {
+ margin-bottom: 1.6em;
+ max-width: 100%;
+}
+
+.fb_iframe_widget span {
+ max-width: 100%;
+}
+
+/* Gist */
+.gist table {
+ table-layout: auto;
+}
+
+.site .gist .gist-file {
+ margin-bottom: 1.5em;
+}
+
+/* Googlemaps */
+.googlemaps {
+ margin-bottom: 1.6em;
+}
+
+.googlemaps iframe {
+ margin-bottom: 0;
+}
+
+/* Crowdsignal */
+.PDS_Poll,
+.CSS_Poll {
+ display: block !important;
+ margin-bottom: 1.6em;
+}
+
+.PDS_Poll .pds-box,
+.CSS_Poll .css-box {
+ max-width: 100%;
+ width: auto;
+}
+
+/* Presentation */
+.site .presentation-wrapper {
+ margin: 0 auto 1.6em;
+}
+
+/* Recipes */
+.site .jetpack-recipe {
+ border: 0;
+ margin: 0 0 1.6em;
+ padding: 0;
+}
+
+.site .jetpack-recipe-title {
+ border: 0;
+ margin-top: 0;
+ padding: 0;
+}
+
+.site .jetpack-recipe .jetpack-recipe-meta {
+ font-size: inherit;
+ margin: 0;
+}
+
+/* Slideshow */
+.site .slideshow-window {
+ border-radius: 0;
+ margin-bottom: 1.6em;
+}
+
+/* Twitter-timeline */
+iframe[id*="twitter-widget-"] {
+ display: block;
+}
+
+/* Vine */
+.vine-embed {
+ display: block;
+}
+
+/* VideoPress */
+.site .video-player {
+ margin-bottom: 1.6em;
+ padding: 0;
+}
+
+.video-player object {
+ margin-bottom: 0;
+}
+
+
+/**
+ * Tiled gallery
+ */
+
+.site .tiled-gallery {
+ margin-bottom: 1.6em;
+}
+
+
+/**
+ * Jetpack Comments
+ */
+
+.comment-form iframe {
+ margin: 0;
+}
+
+.comment-form .subscribe-label {
+ font-weight: 400;
+ text-transform: none;
+}
+
+.comment-subscription-form {
+ font-size: 12px;
+ font-size: 1.2rem;
+ line-height: 1.5em;
+ margin: 2em 0 0;
+}
+
+.comment-subscription-form + .comment-subscription-form {
+ margin-top: 1em;
+}
+
+
+/**
+ * Sharing
+ */
+
+.hentry div.sharedaddy h3.sd-title,
+.hentry h3.sd-title {
+ font-family: "Noto Sans", sans-serif;
+ font-size: 12px;
+ font-size: 1.2rem;
+ line-height: 1;
+ margin: 0 0 1em 0;
+ opacity: 0.7;
+ text-transform: uppercase;
+}
+
+
+/**
+ * Related Posts
+ */
+
+.hentry #jp-relatedposts {
+ margin: 0;
+ padding-top: 0;
+}
+
+.hentry .jp-relatedposts-post-title a {
+ border-bottom: 0;
+}
+
+.hentry .jp-relatedposts-headline em:before {
+ opacity: 0.7;
+}
+
+.hentry div#jp-relatedposts h3.jp-relatedposts-headline {
+ font-family: "Noto Sans", sans-serif;
+ font-size: 12px;
+ font-size: 1.2rem;
+ opacity: 0.7;
+ text-transform: uppercase;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items-visual {
+ margin-right: 0;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post {
+ margin-bottom: 1.6em;
+ opacity: 1;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+.hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 1.2rem;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.hentry .jp-relatedposts-post-title {
+ font-weight: 700;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a {
+ font-weight: inherit;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items div.jp-relatedposts-post:hover .jp-relatedposts-post-title a,
+.hentry div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a {
+ text-decoration: none;
+}
+
+.hentry div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-context,
+.hentry div#jp-relatedposts div.jp-relatedposts-items-visual div.jp-relatedposts-post-nothumbs p.jp-relatedposts-post-excerpt {
+ opacity: 0.7;
+}
+
+.hentry .jp-relatedposts-post-img {
+ margin-bottom: 0.6em;
+}
+
+
+/**
+ * Stats
+ */
+
+#wpstats {
+ display: none;
+}
+
+
+/**
+ * Media Queries
+ */
+
+@media screen and (min-width: 46.25em) {
+ .site .portfolio-entry {
+ margin-bottom: 1.6471em;
+ }
+
+ .site .portfolio-entry-title,
+ .site .portfolio-entry-meta,
+ .site .portfolio-entry-content {
+ font-size: 1.4rem;
+ font-size: 14px;
+ }
+
+ .site .portfolio-entry-content .more-link:after {
+ top: 3px;
+ }
+
+ .site .hentry .gist .gist-file {
+ margin-bottom: 1.75em;
+ }
+
+ .site .widget .gist .gist-file {
+ margin-bottom: 1.3125em;
+ }
+
+ .hentry .fb_iframe_widget,
+ .hentry .googlemaps,
+ .hentry .PDS_Poll,
+ .hentry .CSS_Poll,
+ .site .hentry .presentation-wrapper,
+ .site .hentry .jetpack-recipe,
+ .site .hentry .slideshow-window,
+ .site .hentry .video-player {
+ margin-bottom: 1.6471em;
+ }
+
+ .widget .fb_iframe_widget,
+ .widget .googlemaps,
+ .widget .PDS_Poll,
+ .widget .CSS_Poll,
+ .site .widget .presentation-wrapper,
+ .site .widget .jetpack-recipe,
+ .site .widget .slideshow-window,
+ .site .widget .video-player {
+ margin-bottom: 1.5em;
+ }
+
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4,
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts img {
+ margin: 0 0 1.5em;
+ }
+
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ line-height: 1.5 !important;
+ margin: 0 0 1.5em !important;
+ }
+
+ .widget-area .widget-grofile h4 {
+ margin: 1.5em 0 0;
+ }
+
+ .widget-area .widget-grofile .grofile-accounts {
+ margin-top: 0.75em;
+ }
+
+ .jetpack_subscription_widget #subscribe-email input {
+ padding: 0.5625em;
+ }
+
+ .widget_rss_links li,
+ .widget_top-posts li,
+ .widget_upcoming_events_widget li {
+ padding: 0.9643em 0;
+ }
+
+ .site .tiled-gallery {
+ margin-bottom: 1.6471em;
+ }
+
+ .jetpack-video-wrapper {
+ margin-bottom: 1.6471em;
+ }
+
+ .comment-subscription-form {
+ font-size: 14px;
+ font-size: 1.4rem;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items .jp-relatedposts-post {
+ margin-bottom: 0;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+ .hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 1.4rem;
+ font-size: 14px;
+ }
+
+ .jp-relatedposts-post-img {
+ margin-bottom: 0.6176em;
+ }
+}
+
+@media screen and (min-width: 55em) {
+ .site .portfolio-entry {
+ margin-bottom: 1.6842em
+ }
+
+ .site .portfolio-entry-title,
+ .site .portfolio-entry-meta,
+ .site .portfolio-entry-content {
+ font-size: 1.6rem;
+ font-size: 16px;
+ }
+
+ .site .portfolio-entry-content .more-link:after {
+ font-size: 24px;
+ top: 0;
+ }
+
+ .site .hentry .gist .gist-file {
+ margin-bottom: 2em;
+ }
+
+ .site .widget .gist .gist-file {
+ margin-bottom: 1.5em;
+ }
+
+ .hentry .fb_iframe_widget,
+ .hentry .googlemaps,
+ .hentry .PDS_Poll,
+ .hentry .CSS_Poll,
+ .site .hentry .presentation-wrapper,
+ .site .hentry .jetpack-recipe,
+ .site .hentry .slideshow-window,
+ .site .hentry .video-player {
+ margin-bottom: 1.6842em;
+ }
+
+ .site .tiled-gallery {
+ margin-bottom: 1.6842em;
+ }
+
+ .jetpack_subscription_widget #subscribe-email input {
+ padding: 0.75em;
+ }
+
+ .widget_rss_links li,
+ .widget_top-posts li,
+ .widget_upcoming_events_widget li {
+ padding: 0.7188em 0;
+ }
+
+ .jetpack-video-wrapper {
+ margin-bottom: 1.6842em;
+ }
+
+ .comment-subscription-form {
+ font-size: 16px;
+ font-size: 1.6rem;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+ .hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 1.6rem;
+ font-size: 16px;
+ line-height: 1.5em;
+ }
+
+ .jp-relatedposts-post-img {
+ margin-bottom: 0.6315em;
+ }
+}
+
+@media screen and (min-width: 59.6875em) {
+ .site .portfolio-entry {
+ margin-bottom: 1.6em
+ }
+
+ .site .portfolio-entry-title,
+ .site .portfolio-entry-meta,
+ .site .portfolio-entry-content {
+ font-size: 1.2rem;
+ font-size: 12px;
+ }
+
+ .site .portfolio-entry-content .more-link:after {
+ font-size: 16px;
+ top: 1px;
+ }
+
+ .site .hentry .gist .gist-file {
+ margin-bottom: 1.5em;
+ }
+
+ .site .widget .gist .gist-file {
+ margin-bottom: 1.125em;
+ }
+
+ .hentry .fb_iframe_widget,
+ .hentry .googlemaps,
+ .hentry .PDS_Poll,
+ .hentry .CSS_Poll,
+ .site .hentry .presentation-wrapper,
+ .site .hentry .jetpack-recipe,
+ .site .hentry .slideshow-window,
+ .site .hentry .video-player {
+ margin-bottom: 1.6em;
+ }
+
+ .site .tiled-gallery {
+ margin-bottom: 1.6em;
+ }
+
+ .jetpack_subscription_widget #subscribe-email input {
+ padding: 0.4583em;
+ }
+
+ .jetpack-video-wrapper {
+ margin-bottom: 1.6em;
+ }
+
+ .widget-area .widget-grofile .grofile-thumbnail {
+ max-width: 100%;
+ }
+
+ .widget_rss_links li,
+ .widget_top-posts li,
+ .widget_upcoming_events_widget li {
+ padding: 0.4583em 0;
+ }
+
+ .comment-subscription-form {
+ font-size: 12px;
+ font-size: 1.2rem;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+ .hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 1.2rem;
+ font-size: 12px;
+ line-height: 1.5;
+ }
+
+ .jp-relatedposts-post-img {
+ margin-bottom: 0.6em;
+ }
+}
+
+@media screen and (min-width: 68.75em) {
+ .site .portfolio-entry {
+ margin-bottom: 1.6471em
+ }
+
+ .site .portfolio-entry-title,
+ .site .portfolio-entry-meta,
+ .site .portfolio-entry-content {
+ font-size: 1.4rem;
+ font-size: 14px;
+ }
+
+ .site .portfolio-entry-content .more-link:after {
+ top: 3px;
+ }
+
+ .site .hentry .gist .gist-file {
+ margin-bottom: 1.75em;
+ }
+
+ .site .widget .gist .gist-file {
+ margin-bottom: 1.3125em;
+ }
+
+ .hentry .fb_iframe_widget,
+ .hentry .googlemaps,
+ .hentry .PDS_Poll,
+ .hentry .CSS_Poll,
+ .site .hentry .presentation-wrapper,
+ .site .hentry .jetpack-recipe,
+ .site .hentry .slideshow-window,
+ .site .hentry .video-player {
+ margin-bottom: 1.6471em;
+ }
+
+ .site .tiled-gallery {
+ margin-bottom: 1.6471em;
+ }
+
+ .jetpack_subscription_widget #subscribe-email input {
+ padding: 0.5em;
+ }
+
+ .widget_rss_links li,
+ .widget_top-posts li,
+ .widget_upcoming_events_widget li {
+ padding: 0.4643em 0;
+ }
+
+ .jetpack-video-wrapper {
+ margin-bottom: 1.6471em;
+ }
+
+ .comment-subscription-form {
+ font-size: 14px;
+ font-size: 1.4rem;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+ .hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 14px;
+ font-size: 1.4rem;
+ }
+
+ .jp-relatedposts-post-img {
+ margin-bottom: 0.6176em;
+ }
+}
+
+@media screen and (min-width: 77.5em) {
+ .site .portfolio-entry {
+ margin-bottom: 1.6842em
+ }
+
+ .site .portfolio-entry-title,
+ .site .portfolio-entry-meta,
+ .site .portfolio-entry-content {
+ font-size: 1.6rem;
+ font-size: 16px;
+ }
+
+ .site .portfolio-entry-content .more-link:after {
+ font-size: 24px;
+ top: 0;
+ }
+
+ .site .hentry .gist .gist-file {
+ margin-bottom: 2em;
+ }
+
+ .site .widget .gist .gist-file {
+ margin-bottom: 1.5em;
+ }
+
+ .hentry .fb_iframe_widget,
+ .hentry .googlemaps,
+ .hentry .PDS_Poll,
+ .hentry .CSS_Poll,
+ .site .hentry .presentation-wrapper,
+ .site .hentry .jetpack-recipe,
+ .site .hentry .slideshow-window,
+ .site .hentry .video-player {
+ margin-bottom: 1.6842em;
+ }
+
+ .widget_rss_links li,
+ .widget_top-posts li,
+ .widget_upcoming_events_widget li {
+ padding: 0.4688em 0;
+ }
+
+ .site .tiled-gallery {
+ margin-bottom: 1.6842em;
+ }
+
+ .jetpack-video-wrapper {
+ margin-bottom: 1.6842em;
+ }
+
+ .hentry div.sharedaddy h3.sd-title,
+ .hentry div#jp-relatedposts h3.jp-relatedposts-headline {
+ font-size: 13px;
+ font-size: 1.3rem;
+ }
+
+ .comment-subscription-form {
+ font-size: 16px;
+ font-size: 1.6rem;
+ }
+
+ .hentry div#jp-relatedposts div.jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+ .hentry div#jp-relatedposts div.jp-relatedposts-items p {
+ font-size: 16px;
+ font-size: 1.6rem;
+ line-height: 1.5em;
+ }
+
+ .jp-relatedposts-post-img {
+ margin-bottom: 0.6315em;
+ }
+}
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentyfifteen.php b/plugins/jetpack/modules/theme-tools/compat/twentyfifteen.php
new file mode 100644
index 00000000..adaa42b7
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentyfifteen.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * Jetpack Compatibility File
+ * See: http://jetpack.com/
+ */
+
+function twentyfifteen_jetpack_setup() {
+ /**
+ * Add theme support for Responsive Videos.
+ */
+ add_theme_support( 'jetpack-responsive-videos' );
+
+ /**
+ * Add theme support for geo-location.
+ */
+ add_theme_support( 'jetpack-geo-location' );
+}
+add_action( 'after_setup_theme', 'twentyfifteen_jetpack_setup' );
+
+function twentyfifteen_init_jetpack() {
+ /**
+ * Add our compat CSS file for custom widget stylings and such.
+ * Set the version equal to filemtime for development builds, and the JETPACK__VERSION for production
+ * or skip it entirely for wpcom.
+ */
+ if ( ! is_admin() ) {
+ $version = false;
+ if ( method_exists( 'Jetpack', 'is_development_version' ) ) {
+ $version = Jetpack::is_development_version() ? filemtime( plugin_dir_path( __FILE__ ) . 'twentyfifteen.css' ) : JETPACK__VERSION;
+ }
+ wp_enqueue_style( 'twentyfifteen-jetpack', plugins_url( 'twentyfifteen.css', __FILE__ ), array(), $version );
+ wp_style_add_data( 'twentyfifteen-jetpack', 'rtl', 'replace' );
+ }
+}
+add_action( 'init', 'twentyfifteen_init_jetpack' );
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentyfourteen-rtl.css b/plugins/jetpack/modules/theme-tools/compat/twentyfourteen-rtl.css
new file mode 100644
index 00000000..66e74dbd
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentyfourteen-rtl.css
@@ -0,0 +1,370 @@
+/**
+ * Jetpack compat stylesheet for Twenty Fourteen.
+ */
+
+#jp-post-flair:empty {
+ display: none;
+}
+
+.site-content #jp-post-flair {
+ margin: 24px 0;
+}
+
+.widget #jp-post-flair {
+ padding-top: 0;
+}
+
+.entry-content div.jp-relatedposts {
+ margin: 0;
+}
+
+div.jp-relatedposts .jp-relatedposts-headline em:after {
+ content: ":";
+}
+
+#page .entry-content div.sharedaddy h3,
+#page .entry-summary div.sharedaddy h3,
+#page .entry-content h3.sd-title,
+#page .entry-summary h3.sd-title,
+#primary div.sharedaddy .jp-relatedposts-headline em,
+.pd-rating,
+.cs-rating {
+ color: #767676;
+ font-size: 11px;
+ text-transform: uppercase;
+}
+
+.pd-rating,
+.cs-rating {
+ min-height: 23px;
+ margin-bottom: 5px;
+}
+
+.wp-multiplayer,
+.PDS_Poll,
+.CSS_Poll,
+.entry-content .slideshow-window {
+ margin-bottom: 24px;
+}
+
+.entry-content .gist table {
+ margin-bottom: 0;
+}
+
+.entry-content .slideshow-window {
+ border-radius: 0;
+}
+
+.entry-content .video-player {
+ padding: 0 0 24px;
+}
+
+.highlander-enabled #respond {
+ margin: 0;
+ padding: 0;
+}
+
+.highlander-enabled #respond h3 {
+ margin: 0 0 24px;
+}
+
+.highlander-enabled #respond h3 small a {
+ width: 24px;
+}
+
+.wpcnt {
+ margin-bottom: 15px; /* 24-9 */
+}
+
+#wpstats {
+ display: none;
+}
+
+img[id*="botd"] {
+ position: absolute;
+}
+
+
+/**
+ * Widgets
+ * -----------------------------------------------------------------------------
+ */
+
+/* About.me Widget*/
+
+.aboutme_widget #am_thumbnail {
+ margin-bottom: 18px;
+}
+
+.aboutme_widget #am_thumbnail img {
+ border: 0;
+ max-width: 100%;
+}
+
+.aboutme_widget #am_name {
+ font-weight: 900;
+ margin: 0;
+}
+
+.aboutme_widget #am_headline {
+ font-size: 14px;
+ line-height: 1.2857142857;
+ margin: 0;
+}
+
+.aboutme_widget #am_bio {
+ margin: 18px 0;
+}
+
+.aboutme_widget #am_bio p {
+ margin-bottom: 6px;
+}
+
+.aboutme_widget #am_bio:empty {
+ margin: 0;
+}
+
+.aboutme_widget #am_services {
+ margin-top: 18px;
+}
+
+/* Akismet Widget */
+
+.widget_akismet_widget a,
+.content-sidebar .widget_akismet_widget a {
+ color: rgba(255,255,255,0.5) !important;
+}
+
+.widget_akismet_widget a:hover,
+.content-sidebar .widget_akismet_widget a:hover {
+ color: rgba(255,255,255,0.7) !important;
+}
+
+/* Authors Widget */
+
+.widget_authors li {
+ margin-bottom: 9px;
+}
+
+.widget_authors li:last-child {
+ margin-bottom: 0;
+}
+
+.widget_authors img {
+ margin-left: 5px;
+}
+
+/* Contact Info Widget */
+
+.widget_contact_info > div {
+ margin-bottom: 18px;
+}
+
+.widget_contact_info > div:last-child {
+ margin-bottom: 0;
+}
+
+/* Display WordPress Posts Widget */
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4 {
+ font-size: 14px;
+ line-height: 1.2857142857;
+ margin: 0 0 9px;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ font-size: 14px;
+ line-height: 1.2857142857 !important;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ margin: 9px 0 18px !important;
+}
+
+/* Follow Button Widget */
+
+.widget_follow_button_widget iframe {
+ margin-bottom: 0;
+}
+
+/* Gravatar & Gravatar Profile Widget */
+
+.widget_gravatar img.avatar,
+.widget-grofile .grofile-thumbnail {
+ height: auto;
+ max-width: 100% !important;
+}
+
+.widget-area .widget-grofile h4 {
+ font-size: 12px;
+ line-height: 1.2857142857;
+ margin: 18px 0 9px;
+ text-transform: uppercase;
+}
+
+.widget-area .widget-grofile .grofile-meta h4 {
+ font-size: 18px;
+ line-height: 1;
+ text-transform: none;
+}
+
+/* Image Widget */
+
+.widget_image .wp-caption,
+.widget_image .wp-caption-text {
+ margin-bottom: 0;
+}
+
+.widget_image img {
+ height: auto;
+}
+
+/* Posts I Like Widget */
+
+.widget_jetpack_posts_i_like .widgets-list-layout li {
+ margin: 0;
+}
+
+/* Recent Comments Widget */
+
+.widget_recent_comments table,
+.widget_recent_comments td {
+ border: 0;
+}
+
+.widget_recent_comments td.recentcommentsavatartop,
+.widget_recent_comments td.recentcommentsavatarend {
+ padding: 5px 0 5px 5px;
+}
+
+.widget_recent_comments td.recentcommentstexttop,
+.widget_recent_comments td.recentcommentstextend {
+ padding: 5px 5px 5px 0;
+ vertical-align: top;
+}
+
+/* Recent Images Widget */
+
+.widget_recent_images img {
+ height: auto;
+}
+
+/* Top Posts & Pages Widget */
+
+.widget_top-posts .widgets-list-layout li {
+ margin-bottom: 0;
+}
+
+/* Twitter(old) Widget */
+
+.widget_twitter li {
+ margin-bottom: 18px;
+}
+
+.widget_twitter li:last-child {
+ margin-bottom: 0;
+}
+
+.widget_twitter iframe {
+ margin: 18px 0 0;
+}
+
+/* List type Widgets */
+
+.widget_jp_blogs_i_follow li,
+.widget_delicious li,
+.widgets-list-layout li,
+.widget_top-clicks li,
+.widget_top-posts li,
+.top_rated li,
+.widget_upcoming_events_widget .upcoming-events li {
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
+ padding: 8px 0 9px;
+}
+
+.widget_jp_blogs_i_follow li:first-child,
+.widgets-list-layout li:first-child,
+.widget_top-clicks li:first-child,
+.widget_top-posts li:first-child,
+.top_rated li:first-child,
+.widget_upcoming_events_widget .upcoming-events li:first-child {
+ border-top: 0;
+}
+
+.content-sidebar .widget_jp_blogs_i_follow li,
+.content-sidebar .widgets-list-layout li,
+.content-sidebar .widget_top-clicks li,
+.content-sidebar .widget_top-posts li,
+.content-sidebar .top_rated li,
+.content-sidebar .widget_upcoming_events_widget .upcoming-events li {
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+.content-sidebar widget-area .widget-grofile h4 {
+ font-weight: 900;
+}
+
+
+/**
+ * Media Queries
+ * -----------------------------------------------------------------------------
+ */
+
+@media screen and (min-width: 1008px) {
+ .footer-sidebar .widget_jp_blogs_i_follow li,
+ .footer-sidebar .widget_jp_blogs_i_follow li,
+ .footer-sidebar .widget_top-clicks li,
+ .footer-sidebar .widget_top-posts li,
+ .footer-sidebar .top_rated li,
+ .footer-sidebar .widget_upcoming_events_widget .upcoming-events li,
+ .primary-sidebar .widget_jp_blogs_i_follow li,
+ .primary-sidebar .widget_jp_blogs_i_follow li,
+ .primary-sidebar .widget_top-clicks li,
+ .primary-sidebar .widget_top-posts li,
+ .primary-sidebar .top_rated li {
+ border-top: 0;
+ padding: 0 0 6px;
+ }
+
+ .footer-sidebar .widget_jp_blogs_i_follow li:last-child,
+ .footer-sidebar .widget_jp_blogs_i_follow li:last-child,
+ .footer-sidebar .widget_top-clicks li:last-child,
+ .footer-sidebar .widget_top-posts li:last-child,
+ .footer-sidebar .top_rated li:last-child,
+ .footer-sidebar .widget_upcoming_events_widget .upcoming-events li:last-child,
+ .primary-sidebar .widget_jp_blogs_i_follow li:last-child,
+ .primary-sidebar .widget_jp_blogs_i_follow li:last-child,
+ .primary-sidebar .widget_top-clicks li:last-child,
+ .primary-sidebar .widget_top-posts li:last-child,
+ .primary-sidebar .top_rated li:last-child {
+ padding: 0;
+ }
+
+ .primary-sidebar .widget_blog_subscription input[type="text"],
+ .footer-sidebar .widget_blog_subscription input[type="text"] {
+ padding: 3px 2px !important;
+ }
+
+ .footer-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4,
+ .primary-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4,
+ .footer-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p,
+ .primary-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p,
+ .footer-sidebar .widget-grofile .grofile-meta h4,
+ .primary-sidebar .widget-grofile .grofile-meta h4 {
+ font-size: 12px;
+ line-height: 1.5;
+ }
+
+ .footer-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p,
+ .primary-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ line-height: 1.5 !important;
+ }
+
+ .footer-sidebar .widget-grofile h4,
+ .primary-sidebar .widget-grofile h4,
+ .footer-sidebar .top_rated div > p:first-of-type,
+ .primary-sidebar .top_rated div > p:first-of-type {
+ font-size: 11px;
+ line-height: 1.6363636363;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentyfourteen.css b/plugins/jetpack/modules/theme-tools/compat/twentyfourteen.css
new file mode 100644
index 00000000..903bc3de
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentyfourteen.css
@@ -0,0 +1,365 @@
+/**
+ * Jetpack compat stylesheet for Twenty Fourteen.
+ */
+
+#jp-post-flair:empty {
+ display: none;
+}
+
+.site-content #jp-post-flair {
+ margin: 24px 0;
+}
+
+.widget #jp-post-flair {
+ padding-top: 0;
+}
+
+.entry-content div.jp-relatedposts {
+ margin: 0;
+}
+
+#page .entry-content div.sharedaddy h3,
+#page .entry-summary div.sharedaddy h3,
+#page .entry-content h3.sd-title,
+#page .entry-summary h3.sd-title,
+#primary div.sharedaddy .jp-relatedposts-headline em,
+.pd-rating,
+.cs-rating {
+ color: #767676;
+ font-size: 11px;
+ text-transform: uppercase;
+}
+
+.pd-rating,
+.cs-rating {
+ min-height: 23px;
+ margin-bottom: 5px;
+}
+
+.wp-multiplayer,
+.PDS_Poll,
+.CSS_Poll,
+.entry-content .slideshow-window {
+ margin-bottom: 24px;
+}
+
+.entry-content .gist table {
+ margin-bottom: 0;
+}
+
+.entry-content .slideshow-window {
+ border-radius: 0;
+}
+
+.entry-content .video-player {
+ padding: 0 0 24px;
+}
+
+.highlander-enabled #respond {
+ margin: 0;
+ padding: 0;
+}
+
+.highlander-enabled #respond h3 {
+ margin: 0 0 24px;
+}
+
+.highlander-enabled #respond h3 small a {
+ width: 24px;
+}
+
+.wpcnt {
+ margin-bottom: 15px; /* 24-9 */
+}
+
+#wpstats {
+ display: none;
+}
+
+img[id*="botd"] {
+ position: absolute;
+}
+
+
+/**
+ * Widgets
+ * -----------------------------------------------------------------------------
+ */
+
+/* About.me Widget*/
+
+.aboutme_widget #am_thumbnail {
+ margin-bottom: 18px;
+}
+
+.aboutme_widget #am_thumbnail img {
+ border: 0;
+ max-width: 100%;
+}
+
+.aboutme_widget #am_name {
+ font-weight: 900;
+ margin: 0;
+}
+
+.aboutme_widget #am_headline {
+ font-size: 14px;
+ line-height: 1.2857142857;
+ margin: 0;
+}
+
+.aboutme_widget #am_bio {
+ margin: 18px 0;
+}
+
+.aboutme_widget #am_bio p {
+ margin-bottom: 6px;
+}
+
+.aboutme_widget #am_bio:empty {
+ margin: 0;
+}
+
+.aboutme_widget #am_services {
+ margin-top: 18px;
+}
+
+/* Akismet Widget */
+
+.widget_akismet_widget a,
+.content-sidebar .widget_akismet_widget a {
+ color: rgba(255,255,255,0.5) !important;
+}
+
+.widget_akismet_widget a:hover,
+.content-sidebar .widget_akismet_widget a:hover {
+ color: rgba(255,255,255,0.7) !important;
+}
+
+/* Authors Widget */
+.widget.widget_authors li > ul {
+ margin-left: 0;
+}
+
+.widget_authors li {
+ margin-bottom: 9px;
+}
+
+.widget_authors li:last-child {
+ margin-bottom: 0;
+}
+
+/* Contact Info Widget */
+
+.widget_contact_info > div {
+ margin-bottom: 18px;
+}
+
+.widget_contact_info > div:last-child {
+ margin-bottom: 0;
+}
+
+/* Display WordPress Posts Widget */
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4 {
+ font-size: 14px;
+ line-height: 1.2857142857;
+ margin: 0 0 9px;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ font-size: 14px;
+ line-height: 1.2857142857 !important;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ margin: 9px 0 18px !important;
+}
+
+/* Follow Button Widget */
+
+.widget_follow_button_widget iframe {
+ margin-bottom: 0;
+}
+
+/* Gravatar & Gravatar Profile Widget */
+
+.widget_gravatar img.avatar,
+.widget-grofile .grofile-thumbnail {
+ height: auto;
+ max-width: 100% !important;
+}
+
+.widget-area .widget-grofile h4 {
+ font-size: 12px;
+ line-height: 1.2857142857;
+ margin: 18px 0 9px;
+ text-transform: uppercase;
+}
+
+.widget-area .widget-grofile .grofile-meta h4 {
+ font-size: 18px;
+ line-height: 1;
+ text-transform: none;
+}
+
+/* Image Widget */
+
+.widget_image .wp-caption,
+.widget_image .wp-caption-text {
+ margin-bottom: 0;
+}
+
+.widget_image img {
+ height: auto;
+}
+
+/* Posts I Like Widget */
+
+.widget_jetpack_posts_i_like .widgets-list-layout li {
+ margin: 0;
+}
+
+/* Recent Comments Widget */
+
+.widget_recent_comments table,
+.widget_recent_comments td {
+ border: 0;
+}
+
+.widget_recent_comments td.recentcommentsavatartop,
+.widget_recent_comments td.recentcommentsavatarend {
+ padding: 5px 5px 5px 0;
+}
+
+.widget_recent_comments td.recentcommentstexttop,
+.widget_recent_comments td.recentcommentstextend {
+ padding: 5px 0 5px 5px;
+ vertical-align: top;
+}
+
+/* Recent Images Widget */
+
+.widget_recent_images img {
+ height: auto;
+}
+
+/* Top Posts & Pages Widget */
+
+.widget_top-posts .widgets-list-layout li {
+ margin-bottom: 0;
+}
+
+/* Twitter(old) Widget */
+
+.widget_twitter li {
+ margin-bottom: 18px;
+}
+
+.widget_twitter li:last-child {
+ margin-bottom: 0;
+}
+
+.widget_twitter iframe {
+ margin: 18px 0 0;
+}
+
+/* List type Widgets */
+
+.widget_jp_blogs_i_follow li,
+.widget_delicious li,
+.widgets-list-layout li,
+.widget_top-clicks li,
+.widget_top-posts li,
+.top_rated li,
+.widget_upcoming_events_widget .upcoming-events li {
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
+ padding: 8px 0 9px;
+}
+
+.widget_jp_blogs_i_follow li:first-child,
+.widgets-list-layout li:first-child,
+.widget_top-clicks li:first-child,
+.widget_top-posts li:first-child,
+.top_rated li:first-child,
+.widget_upcoming_events_widget .upcoming-events li:first-child {
+ border-top: 0;
+}
+
+.content-sidebar .widget_jp_blogs_i_follow li,
+.content-sidebar .widgets-list-layout li,
+.content-sidebar .widget_top-clicks li,
+.content-sidebar .widget_top-posts li,
+.content-sidebar .top_rated li,
+.content-sidebar .widget_upcoming_events_widget .upcoming-events li {
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+.content-sidebar widget-area .widget-grofile h4 {
+ font-weight: 900;
+}
+
+
+/**
+ * Media Queries
+ * -----------------------------------------------------------------------------
+ */
+
+@media screen and (min-width: 1008px) {
+ .footer-sidebar .widget_jp_blogs_i_follow li,
+ .footer-sidebar .widget_jp_blogs_i_follow li,
+ .footer-sidebar .widget_top-clicks li,
+ .footer-sidebar .widget_top-posts li,
+ .footer-sidebar .top_rated li,
+ .footer-sidebar .widget_upcoming_events_widget .upcoming-events li,
+ .primary-sidebar .widget_jp_blogs_i_follow li,
+ .primary-sidebar .widget_jp_blogs_i_follow li,
+ .primary-sidebar .widget_top-clicks li,
+ .primary-sidebar .widget_top-posts li,
+ .primary-sidebar .top_rated li {
+ border-top: 0;
+ padding: 0 0 6px;
+ }
+
+ .footer-sidebar .widget_jp_blogs_i_follow li:last-child,
+ .footer-sidebar .widget_jp_blogs_i_follow li:last-child,
+ .footer-sidebar .widget_top-clicks li:last-child,
+ .footer-sidebar .widget_top-posts li:last-child,
+ .footer-sidebar .top_rated li:last-child,
+ .footer-sidebar .widget_upcoming_events_widget .upcoming-events li:last-child,
+ .primary-sidebar .widget_jp_blogs_i_follow li:last-child,
+ .primary-sidebar .widget_jp_blogs_i_follow li:last-child,
+ .primary-sidebar .widget_top-clicks li:last-child,
+ .primary-sidebar .widget_top-posts li:last-child,
+ .primary-sidebar .top_rated li:last-child {
+ padding: 0;
+ }
+
+ .primary-sidebar .widget_blog_subscription input[type="text"],
+ .footer-sidebar .widget_blog_subscription input[type="text"] {
+ padding: 3px 2px !important;
+ }
+
+ .footer-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4,
+ .primary-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4,
+ .footer-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p,
+ .primary-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p,
+ .footer-sidebar .widget-grofile .grofile-meta h4,
+ .primary-sidebar .widget-grofile .grofile-meta h4 {
+ font-size: 12px;
+ line-height: 1.5;
+ }
+
+ .footer-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p,
+ .primary-sidebar .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ line-height: 1.5 !important;
+ }
+
+ .footer-sidebar .widget-grofile h4,
+ .primary-sidebar .widget-grofile h4,
+ .footer-sidebar .top_rated div > p:first-of-type,
+ .primary-sidebar .top_rated div > p:first-of-type {
+ font-size: 11px;
+ line-height: 1.6363636363;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentyfourteen.php b/plugins/jetpack/modules/theme-tools/compat/twentyfourteen.php
new file mode 100644
index 00000000..202dfbf3
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentyfourteen.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * A last try to show posts, in case the Featured Content plugin returns no IDs.
+ *
+ * @param array $featured_ids
+ * @return array
+ */
+function twentyfourteen_featured_content_post_ids( $featured_ids ) {
+ if ( empty( $featured_ids ) ) {
+ $featured_ids = array_slice( get_option( 'sticky_posts', array() ), 0, 6 );
+ }
+
+ return $featured_ids;
+}
+add_action( 'featured_content_post_ids', 'twentyfourteen_featured_content_post_ids' );
+
+/**
+ * Set the default tag name for Featured Content.
+ *
+ * @param WP_Customize_Manager $wp_customize Theme Customizer object.
+ * @return void
+ */
+function twentyfourteen_customizer_default( $wp_customize ) {
+ $wp_customize->get_setting( 'featured-content[tag-name]' )->default = 'featured';
+}
+add_action( 'customize_register', 'twentyfourteen_customizer_default' );
+
+/**
+ * Sets a default tag of 'featured' for Featured Content.
+ *
+ * @param array $settings
+ * @return array
+ */
+function twentyfourteen_featured_content_default_settings( $settings ) {
+ $settings['tag-name'] = 'featured';
+
+ return $settings;
+}
+add_action( 'featured_content_default_settings', 'twentyfourteen_featured_content_default_settings' );
+
+/**
+ * Removes sharing markup from post content if we're not in the loop and it's a
+ * formatted post.
+ *
+ * @param bool $show Whether to show sharing options.
+ * @param WP_Post $post The post to share.
+ * @return bool
+ */
+function twentyfourteen_mute_content_filters( $show, $post ) {
+ $formats = get_theme_support( 'post-formats' );
+ if ( ! in_the_loop() && has_post_format( $formats[0], $post ) ) {
+ $show = false;
+ }
+ return $show;
+}
+add_filter( 'sharing_show', 'twentyfourteen_mute_content_filters', 10, 2 );
+
+function twentyfourteen_init_jetpack() {
+ /**
+ * Add our compat CSS file for custom widget stylings and such.
+ * Set the version equal to filemtime for development builds, and the JETPACK__VERSION for production.
+ */
+ if ( ! is_admin() ) {
+ $version = false;
+ if ( method_exists( 'Jetpack', 'is_development_version' ) ) {
+ $version = Jetpack::is_development_version() ? filemtime( plugin_dir_path( __FILE__ ) . 'twentyfourteen.css' ) : JETPACK__VERSION;
+ }
+ wp_enqueue_style( 'twentyfourteen-jetpack', plugins_url( 'twentyfourteen.css', __FILE__ ), array(), $version );
+ wp_style_add_data( 'twentyfourteen-jetpack', 'rtl', 'replace' );
+ }
+}
+add_action( 'init', 'twentyfourteen_init_jetpack' );
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentynineteen-rtl.css b/plugins/jetpack/modules/theme-tools/compat/twentynineteen-rtl.css
new file mode 100644
index 00000000..2cf7d0bb
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentynineteen-rtl.css
@@ -0,0 +1 @@
+.infinite-scroll .pagination,.infinite-scroll .posts-navigation,.infinite-scroll.neverending .site-footer{display:none}.infinity-end.neverending .site-footer{display:block}.infinite-loader{margin:calc(3 * 1rem) auto}.infinite-loader .spinner{margin:0 auto;right:inherit!important}.site-main #infinite-handle{margin:calc(2 * 1rem) auto}.site-main #infinite-handle span{background:100% 0;display:block;font-size:.8888888889em;text-align:center}.site-main #infinite-handle span button,.site-main #infinite-handle span button:focus,.site-main #infinite-handle span button:hover{transition:background 150ms ease-in-out;background:#0073aa;border:none;border-radius:5px;box-sizing:border-box;color:#fff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:.8888888889em;font-weight:600;line-height:1.2;outline:0;padding:.66rem 1rem}.site-main #infinite-handle span button:hover{cursor:pointer}.site-main #infinite-handle span button:focus,.site-main #infinite-handle span button:hover{background:#111}.site-main #infinite-handle span button:focus{outline:thin dotted;outline-offset:-4px}.site-main .infinite-wrap .entry:first-of-type{margin-top:calc(6 * 1rem)}.entry .jetpack-video-wrapper{margin-bottom:1.75em}.sd-block{line-height:1}.entry div.sharedaddy h3.sd-title,.entry h3.sd-title{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1.125em;font-weight:700;letter-spacing:-.02em;line-height:1.2;margin-bottom:.5em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.entry div.sharedaddy h3.sd-title:before,.entry h3.sd-title:before{background:#767676;border-top:none;content:"\020";display:block;height:2px;margin:1rem 0;width:1em;min-width:inherit}.sd-social-icon-text .sd-content ul,.sd-social-text .sd-content ul{margin-bottom:-.3125em!important}.sd-social-icon .sd-content ul,.sd-social-official .sd-content ul{margin-bottom:0!important}.entry #sharing_email .sharing_send,.entry .sd-content ul li .option a.share-ustom,.entry .sd-content ul li a.sd-button,.entry .sd-content ul li.advanced a.share-more,.entry .sd-content ul li.preview-item div.option.option-smart-off a,.entry .sd-social-icon .sd-content ul li a.sd-button,.entry .sd-social-icon-text .sd-content ul li a.sd-button,.entry .sd-social-official .sd-content>ul>li .digg_button>a,.entry .sd-social-official .sd-content>ul>li>a.sd-button,.entry .sd-social-text .sd-content ul li a.sd-button{box-shadow:none}.entry #jp-relatedposts{padding-top:0;margin-top:32px;margin-bottom:32px}.entry #jp-relatedposts h3.jp-relatedposts-headline{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1.125em;font-weight:700;letter-spacing:-.02em;line-height:1.2;margin-bottom:.5em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.entry #jp-relatedposts h3.jp-relatedposts-headline:before{background:#767676;border-top:none;content:"\020";display:block;height:2px;margin:1rem 0;width:1em;min-width:inherit}.entry #jp-relatedposts h3.jp-relatedposts-headline em:before{display:none}.entry #jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post{opacity:1}.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post,.entry #jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1em;letter-spacing:-.02em;line-height:1.2;margin-bottom:.5em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post span a,.entry #jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title a{font-weight:700}.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-excerpt,.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title{margin-top:.5em;margin-bottom:.5em}.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-context,.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-date{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:13px;font-weight:500}.entry #jp-relatedposts .jp-relatedposts-items p,.entry #jp-relatedposts .jp-relatedposts-items-visual .entry h4.jp-relatedposts-post-title{letter-spacing:normal}#wpstats{display:none}.comments-area .comments-title-wrap+.comment-respond .comment-reply-title{display:none}.widget_author_grid ul,.widget_authors ul,.widget_jp_blogs_i_follow ul,.widget_links ul,.widget_rss_links ul{list-style:none;padding-right:0;padding-left:0}.widget_jp_blogs_i_follow li,.widget_links li,.widget_rss_links li{color:#767676;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:calc(22px * 1.125);font-weight:700;line-height:1.2;margin-top:.5rem;margin-bottom:.5rem}.widget.widget_authors ul li>ul{list-style-type:disc;padding-right:4.25rem}.rtl .widget.widget_authors ul li>ul{padding-right:0;padding-left:1rem}.widget_authors>ul>li>a{display:block;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif;position:relative}.widget.widget_authors li a strong{line-height:1.2;position:absolute;top:0}.widget.widget_authors .avatar{float:right;margin-left:1em}.widget_authors li>ul{clear:both}.widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4{font-size:100%;margin:1rem 0}.widget_goodreads div[class^=gr_custom_container]{border:none}.widget_goodreads div[class^=gr_custom_each_container]{border-bottom:none;margin-bottom:1rem;padding-bottom:0}.widget_goodreads div[class^=gr_custom_author],.widget_goodreads h2[class^=gr_custom_header]{font-size:inherit;line-height:1.15}.widget_eu_cookie_law_widget #eu-cookie-law{border-color:#ccc;color:#767676;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:.68182em;padding:.5rem 1rem}.widget_eu_cookie_law_widget #eu-cookie-law .accept{font-size:1em;padding:10px 12px}.widget_rss li a.rsswidget{display:block;margin:1rem 0 .5rem}.widget_rss .rssSummary{font-family:NonBreakingSpaceOverride,"Hoefler Text","Baskerville Old Face",Garamond,"Times New Roman",serif;font-weight:400;font-size:22px}.widget_rss .rss-date,.widget_rss cite{color:#9c9c9c;display:block;font-size:.71111em;font-weight:500;margin:.5rem 0}.widget_rss cite{font-style:normal}.widget_rss cite:before{content:"\2014\00a0"}.widget_top-posts .widgets-list-layout-links{float:inherit;margin-right:calc(40px + 1rem);width:inherit}@media only screen and (min-width:600px){.widget.widget_search .search-field{max-width:calc(50vw - 20%)}}.twentynineteen-customizer .entry .entry-footer>span,.twentynineteen-customizer .entry .entry-meta>span{display:inline} \ No newline at end of file
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentynineteen.css b/plugins/jetpack/modules/theme-tools/compat/twentynineteen.css
new file mode 100644
index 00000000..4ba953f0
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentynineteen.css
@@ -0,0 +1,374 @@
+/* Infinite scroll */
+
+/* Globally hidden elements when Infinite Scroll is supported and in use. */
+.infinite-scroll .pagination,
+.infinite-scroll .posts-navigation,
+.infinite-scroll.neverending .site-footer {
+ /* Theme Footer (when set to scrolling) */
+ display: none;
+}
+
+ /* When Infinite Scroll has reached its end we need to re-display elements that were hidden (via .neverending) before. */
+.infinity-end.neverending .site-footer {
+ display: block;
+}
+
+.infinite-loader {
+ margin: calc(3 * 1rem) auto;
+}
+
+.infinite-loader .spinner {
+ margin: 0 auto;
+ left: inherit !important;
+}
+
+.site-main #infinite-handle {
+ margin: calc(2 * 1rem) auto;
+}
+
+.site-main #infinite-handle span {
+ background: transparent;
+ display: block;
+ font-size: 0.8888888889em;
+ text-align: center;
+}
+
+.site-main #infinite-handle span button,
+.site-main #infinite-handle span button:hover,
+.site-main #infinite-handle span button:focus {
+ transition: background 150ms ease-in-out;
+ background: #0073aa;
+ border: none;
+ border-radius: 5px;
+ box-sizing: border-box;
+ color: white;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ font-size: 0.8888888889em;
+ font-weight: 600;
+ line-height: 1.2;
+ outline: none;
+ padding: 0.66rem 1rem;
+}
+
+.site-main #infinite-handle span button:hover {
+ cursor: pointer;
+}
+
+.site-main #infinite-handle span button:hover,
+.site-main #infinite-handle span button:focus {
+ background: #111;
+}
+
+.site-main #infinite-handle span button:focus {
+ outline: thin dotted;
+ outline-offset: -4px;
+}
+
+.site-main .infinite-wrap .entry:first-of-type {
+ margin-top: calc(6 * 1rem);
+}
+
+/**
+ * Responsive Videos
+ */
+.entry .jetpack-video-wrapper {
+ margin-bottom: 1.75em;
+}
+
+/**
+ * Sharing
+ */
+
+.sd-block {
+ line-height: 1;
+}
+
+.entry div.sharedaddy h3.sd-title,
+.entry h3.sd-title {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ font-size: 1.125em;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ line-height: 1.2;
+ margin-bottom: 0.5em;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.entry div.sharedaddy h3.sd-title:before,
+.entry h3.sd-title:before {
+ background: #767676;
+ border-top: none;
+ content: "\020";
+ display: block;
+ height: 2px;
+ margin: 1rem 0;
+ width: 1em;
+ min-width: inherit;
+}
+
+.sd-social-icon-text .sd-content ul,
+.sd-social-text .sd-content ul {
+ margin-bottom: -0.3125em !important;
+}
+
+.sd-social-icon .sd-content ul,
+.sd-social-official .sd-content ul {
+ margin-bottom: 0 !important;
+}
+
+.entry #sharing_email .sharing_send,
+.entry .sd-content ul li .option a.share-ustom,
+.entry .sd-content ul li a.sd-button,
+.entry .sd-content ul li.advanced a.share-more,
+.entry .sd-content ul li.preview-item div.option.option-smart-off a,
+.entry .sd-social-icon .sd-content ul li a.sd-button,
+.entry .sd-social-icon-text .sd-content ul li a.sd-button,
+.entry .sd-social-official .sd-content > ul > li .digg_button > a,
+.entry .sd-social-official .sd-content > ul > li > a.sd-button,
+.entry .sd-social-text .sd-content ul li a.sd-button {
+ box-shadow: none;
+}
+
+
+/**
+ * Related Posts
+ */
+
+.entry #jp-relatedposts {
+ padding-top: 0;
+ margin-top: 32px;
+ margin-bottom: 32px;
+}
+
+.entry #jp-relatedposts h3.jp-relatedposts-headline {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ font-size: 1.125em;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ line-height: 1.2;
+ margin-bottom: 0.5em;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.entry #jp-relatedposts h3.jp-relatedposts-headline:before {
+ background: #767676;
+ border-top: none;
+ content: "\020";
+ display: block;
+ height: 2px;
+ margin: 1rem 0;
+ width: 1em;
+ min-width: inherit;
+}
+
+.entry #jp-relatedposts h3.jp-relatedposts-headline em:before {
+ display: none;
+}
+
+.entry #jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post {
+ opacity: 1;
+}
+
+.entry #jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title,
+.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ font-size: 1em;
+ letter-spacing: -0.02em;
+ line-height: 1.2;
+ margin-bottom: 0.5em;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.entry #jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title a,
+.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post span a {
+ font-weight: 700;
+}
+
+.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title,
+.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-excerpt {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-context,
+.entry #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-date {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ font-size: 13px;
+ font-weight: 500;
+}
+
+.entry #jp-relatedposts .jp-relatedposts-items p,
+.entry #jp-relatedposts .jp-relatedposts-items-visual
+.entry h4.jp-relatedposts-post-title {
+ letter-spacing: normal;
+}
+
+
+/**
+ * Stats
+ */
+
+#wpstats {
+ display: none;
+}
+
+
+/**
+ * Comments
+ */
+
+.comments-area .comments-title-wrap + .comment-respond .comment-reply-title {
+ display: none;
+}
+
+
+/**
+ * Widgets
+ */
+
+/* Widget List Resets */
+.widget_authors ul,
+.widget_author_grid ul,
+.widget_jp_blogs_i_follow ul,
+.widget_links ul,
+.widget_rss_links ul {
+ list-style: none;
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.widget_links li,
+.widget_jp_blogs_i_follow li,
+.widget_rss_links li {
+ color: #767676;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ font-size: calc(22px * 1.125);
+ font-weight: 700;
+ line-height: 1.2;
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+
+/* Authors Widget */
+.widget.widget_authors ul li > ul {
+ list-style-type: disc;
+ padding-left: 4.25rem;
+}
+
+.rtl .widget.widget_authors ul li > ul {
+ padding-left: 0;
+ padding-right: 1rem;
+}
+
+.widget_authors > ul > li > a {
+ display: block;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ position: relative;
+}
+
+.widget.widget_authors li a strong {
+ line-height: 1.2;
+ position: absolute;
+ top: 0;
+}
+
+.widget.widget_authors .avatar {
+ float: left;
+ margin-right: 1em;
+}
+
+.widget_authors li > ul {
+ clear: both;
+}
+
+/* Display WordPress Posts */
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4 {
+ font-size: 100%;
+ margin: 1rem 0;
+}
+
+/* GoodReads */
+.widget_goodreads div[class^="gr_custom_container"] {
+ border: none;
+}
+
+.widget_goodreads div[class^="gr_custom_each_container"] {
+ border-bottom: none;
+ margin-bottom: 1rem;
+ padding-bottom: 0;
+}
+
+.widget_goodreads h2[class^="gr_custom_header"],
+.widget_goodreads div[class^="gr_custom_author"] {
+ font-size: inherit;
+ line-height: 1.15;
+}
+
+/* EU cookie law */
+.widget_eu_cookie_law_widget #eu-cookie-law {
+ border-color: #ccc;
+ color: #767676;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ font-size: 0.68182em;
+ padding: 0.5rem 1rem;
+}
+
+.widget_eu_cookie_law_widget #eu-cookie-law .accept {
+ font-size: 1em;
+ padding: 10px 12px;
+}
+
+/* RSS Feed Widget */
+.widget_rss li a.rsswidget {
+ display: block;
+ margin: 1rem 0 0.5rem;
+}
+
+.widget_rss .rssSummary {
+ font-family: "NonBreakingSpaceOverride", "Hoefler Text", "Baskerville Old Face", Garamond, "Times New Roman", serif;
+ font-weight: 400;
+ font-size: 22px;
+}
+
+.widget_rss cite,
+.widget_rss .rss-date {
+ color: #9c9c9c;
+ display: block;
+ font-size: 0.71111em;
+ font-weight: 500;
+ margin: 0.5rem 0;
+}
+
+.widget_rss cite {
+ font-style: normal;
+}
+
+.widget_rss cite:before {
+ content: "\2014\00a0";
+}
+
+/* Top Posts & Pages Widget */
+.widget_top-posts .widgets-list-layout-links {
+ float: inherit;
+ margin-left: calc(40px + 1rem);
+ width: inherit;
+}
+
+/* Search widget override */
+@media only screen and (min-width: 600px) {
+ .widget.widget_search .search-field {
+ max-width: calc( 50vw - 20% );
+ }
+}
+
+/**
+ * Content Options
+ */
+.twentynineteen-customizer .entry .entry-meta > span,
+.twentynineteen-customizer .entry .entry-footer > span {
+ display: inline;
+}
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentynineteen.php b/plugins/jetpack/modules/theme-tools/compat/twentynineteen.php
new file mode 100644
index 00000000..fcc8b8c1
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentynineteen.php
@@ -0,0 +1,126 @@
+<?php
+/**
+ * Jetpack Compatibility File
+ * See: http://jetpack.com/
+ */
+
+function twentynineteen_jetpack_setup() {
+
+ /**
+ * Add theme support for Infinite Scroll.
+ */
+ add_theme_support( 'infinite-scroll', array(
+ 'type' => 'click',
+ 'container' => 'main',
+ 'render' => 'twentynineteen_infinite_scroll_render',
+ 'footer' => 'page',
+ ) );
+
+ /**
+ * Add theme support for Responsive Videos.
+ */
+ add_theme_support( 'jetpack-responsive-videos' );
+
+ /**
+ * Add theme support for geo-location.
+ */
+ add_theme_support( 'jetpack-geo-location' );
+
+ /**
+ * Add theme support for Content Options.
+ */
+ add_theme_support( 'jetpack-content-options', array(
+ 'blog-display' => array( 'content', 'excerpt' ),
+ 'post-details' => array(
+ 'stylesheet' => 'twentynineteen-style',
+ 'date' => '.posted-on',
+ 'categories' => '.cat-links',
+ 'tags' => '.tags-links',
+ 'author' => '.byline',
+ 'comment' => '.comments-link',
+ ),
+ 'featured-images' => array(
+ 'archive' => true,
+ 'post' => true,
+ 'page' => true,
+ ),
+ ) );
+}
+add_action( 'after_setup_theme', 'twentynineteen_jetpack_setup' );
+
+/**
+ * Custom render function for Infinite Scroll.
+ */
+function twentynineteen_infinite_scroll_render() {
+ while ( have_posts() ) {
+ the_post();
+ get_template_part( 'template-parts/content/content' );
+ }
+}
+
+function twentynineteen_init_jetpack() {
+ /**
+ * Add our compat CSS file for Infinite Scroll and custom widget stylings and such.
+ * Set the version equal to filemtime for development builds, and the JETPACK__VERSION for production
+ * or skip it entirely for wpcom.
+ */
+ if ( ! is_admin() ) {
+ $version = false;
+ if ( method_exists( 'Jetpack', 'is_development_version' ) ) {
+ $version = Jetpack::is_development_version() ? filemtime( plugin_dir_path( __FILE__ ) . 'twentynineteen.css' ) : JETPACK__VERSION;
+ }
+ wp_enqueue_style( 'twentynineteen-jetpack', plugins_url( 'twentynineteen.css', __FILE__ ), array(), $version );
+ wp_style_add_data( 'twentynineteen-jetpack', 'rtl', 'replace' );
+ }
+}
+add_action( 'init', 'twentynineteen_init_jetpack' );
+
+/**
+ * Alter gallery widget default width.
+ */
+function twentynineteen_gallery_widget_content_width( $width ) {
+ return 390;
+}
+add_filter( 'gallery_widget_content_width', 'twentynineteen_gallery_widget_content_width' );
+
+/**
+ * Alter featured-image default visibility for content-options.
+ */
+function twentynineteen_override_post_thumbnail( $width ) {
+ $options = get_theme_support( 'jetpack-content-options' );
+ $featured_images = ( ! empty( $options[0]['featured-images'] ) ) ? $options[0]['featured-images'] : null;
+
+ $settings = array(
+ 'post-default' => ( isset( $featured_images['post-default'] ) && false === $featured_images['post-default'] ) ? '' : 1,
+ 'page-default' => ( isset( $featured_images['page-default'] ) && false === $featured_images['page-default'] ) ? '' : 1,
+ );
+
+ $settings = array_merge( $settings, array(
+ 'post-option' => get_option( 'jetpack_content_featured_images_post', $settings['post-default'] ),
+ 'page-option' => get_option( 'jetpack_content_featured_images_page', $settings['page-default'] ),
+ ) );
+
+ if ( ( ! $settings['post-option'] && is_single() )
+ || ( ! $settings['page-option'] && is_singular() && is_page() ) ) {
+ return false;
+ } else {
+ return ! post_password_required() && ! is_attachment() && has_post_thumbnail();
+ }
+}
+add_filter( 'twentynineteen_can_show_post_thumbnail', 'twentynineteen_override_post_thumbnail', 10, 2 );
+
+/**
+ * Adds custom classes to the array of body classes.
+ *
+ * @param array $classes Classes for the body element.
+ * @return array
+ */
+function twentynineteen_jetpack_body_classes( $classes ) {
+ // Adds a class if we're in the Customizer
+ if ( is_customize_preview() ) :
+ $classes[] = 'twentynineteen-customizer';
+ endif;
+
+ return $classes;
+}
+add_filter( 'body_class', 'twentynineteen_jetpack_body_classes' );
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentyseventeen.php b/plugins/jetpack/modules/theme-tools/compat/twentyseventeen.php
new file mode 100644
index 00000000..4a60e504
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentyseventeen.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * Jetpack Compatibility File
+ * See: http://jetpack.com/
+ */
+
+function twentyseventeen_jetpack_setup() {
+ /**
+ * Add theme support for geo-location.
+ */
+ add_theme_support( 'jetpack-geo-location' );
+}
+add_action( 'after_setup_theme', 'twentyseventeen_jetpack_setup' );
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentysixteen-rtl.css b/plugins/jetpack/modules/theme-tools/compat/twentysixteen-rtl.css
new file mode 100644
index 00000000..d60c31a4
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentysixteen-rtl.css
@@ -0,0 +1,832 @@
+/**
+ * Jetpack Comments
+ */
+
+.comment-form iframe {
+ margin: 0;
+}
+
+.comment-form .subscribe-label {
+ font-weight: 400;
+ letter-spacing: 0;
+ text-transform: none;
+}
+
+.comment-subscription-form {
+ margin: 1.75em 0 0;
+}
+
+.comment-subscription-form + .comment-subscription-form {
+ margin-top: 0;
+}
+
+
+/**
+ * Extra Widgets
+ */
+
+ /* Blog Subscriptions Widget */
+.jetpack_subscription_widget #subscribe-email input {
+ padding: 0.625em 0.4375em;
+ width: 100%;
+}
+
+.jetpack_subscription_widget form > :last-child {
+ margin-bottom: 0;
+}
+
+ /* Contact Info Widget */
+.widget_contact_info .contact-map {
+ margin-bottom: 1.75em;
+}
+
+/* Display WordPress Posts Widget */
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts {
+ margin: 0;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4 {
+ font-size: inherit;
+ margin: 0 0 0.875em;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts img {
+ margin-bottom: 0.875em;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ font-size: inherit;
+ line-height: 1.75 !important;
+ margin: 0 0 1.75em !important;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts > :last-child {
+ margin-bottom: 0 !important;
+}
+
+/* Gallery Widget */
+.widget-gallery .slideshow-window {
+ border-radius: 0;
+}
+
+/* Goodreads Widget */
+.widget_goodreads div[class^="gr_custom_container"] {
+ background-color: transparent;
+ border: 0;
+ border-radius: 0;
+ color: inherit;
+ padding: 0;
+}
+
+.widget_goodreads div[class^="gr_custom_container"] a {
+ color: inherit;
+}
+
+.widget_goodreads div[class^="gr_custom_each_container"] {
+ border: 0;
+ margin-bottom: 1.75em;
+ padding-bottom: 0;
+}
+
+.widget_goodreads h2[class^="gr_custom_header"],
+.widget_goodreads div[class^="gr_custom_author"] {
+ font-size: inherit;
+}
+
+/* Gravatar Profile Widget */
+.widget-grofile .grofile-thumbnail {
+ width: 300px;
+}
+
+.widget-area .widget-grofile h4 {
+ font-size: inherit;
+ font-weight: 900;
+ margin: 1.75em 0 0;
+}
+
+.widget-area .widget-grofile .grofile-accounts {
+ margin-top: 0.4375em;
+}
+
+/* Image Widget */
+.widget_image .wp-caption {
+ margin-bottom: 0;
+}
+
+/* RSS Links Widget */
+.widget_rss_links img {
+ position: relative;
+ top: -1px;
+}
+
+/* Social Media Icon Widget */
+.widget.widget_wpcom_social_media_icons_widget ul {
+ margin: 0 0 -0.4375em;
+}
+
+.widget.widget_wpcom_social_media_icons_widget ul:before,
+.widget.widget_wpcom_social_media_icons_widget ul:after {
+ content: "";
+ display: table;
+}
+
+.widget.widget_wpcom_social_media_icons_widget ul:after {
+ clear: both;
+}
+
+.widget.widget_wpcom_social_media_icons_widget li {
+ float: right;
+ margin: 0 0 0.4375em 0.4375em;
+}
+
+.widget.widget_wpcom_social_media_icons_widget li a {
+ border: 1px solid currentColor;
+ border-radius: 50%;
+ color: inherit;
+ display: block;
+ height: 35px;
+ position: relative;
+ width: 35px;
+}
+
+.widget.widget_wpcom_social_media_icons_widget li a:before {
+ height: 33px;
+ line-height: 33px;
+ text-align: center;
+ width: 33px;
+}
+
+.widget.widget_wpcom_social_media_icons_widget li a:hover:before,
+.widget.widget_wpcom_social_media_icons_widget li a:focus:before {
+ opacity: 0.8;
+}
+
+/* Social Icons Widget */
+.widget.jetpack_widget_social_icons ul {
+ margin: 0 0 -0.4375em;
+}
+
+.widget.jetpack_widget_social_icons ul:before,
+.widget.jetpack_widget_social_icons ul:after {
+ content: "";
+ display: table;
+}
+
+.widget.jetpack_widget_social_icons ul:after {
+ clear: both;
+}
+
+.widget.jetpack_widget_social_icons li {
+ float: right;
+ margin: 0 0 0.4375em 0.4375em;
+}
+
+.widget.jetpack_widget_social_icons li a {
+ border: 1px solid currentColor;
+ border-radius: 50%;
+ color: inherit;
+ display: block;
+ position: relative;
+}
+
+.widget.jetpack_widget_social_icons li a:hover,
+.widget.jetpack_widget_social_icons li a:focus {
+ opacity: 0.8;
+}
+
+.widget.jetpack_widget_social_icons ul.size-small a {
+ height: 38px;
+ padding: 6px;
+ width: 38px;
+}
+
+.widget.jetpack_widget_social_icons ul.size-small svg {
+ height: 24px;
+ width: 24px;
+}
+
+.widget.jetpack_widget_social_icons ul.size-medium a {
+ height: 50px;
+ padding: 8px;
+ width: 50px;
+}
+
+.widget.jetpack_widget_social_icons ul.size-medium svg {
+ height: 32px;
+ width: 32px;
+}
+
+.widget.jetpack_widget_social_icons ul.size-large a {
+ height: 70px;
+ padding: 10px;
+ width: 70px;
+}
+
+.widget.jetpack_widget_social_icons ul.size-large svg {
+ height: 48px;
+ width: 48px;
+}
+
+/* Top Posts & Pages Widget */
+.widget_top-posts .widgets-list-layout .widgets-list-layout-blavatar {
+ margin-top: 0.25em;
+}
+
+.widget_top-posts .widgets-list-layout-links {
+ width: -webkit-calc(100% - 3.375em);
+ width: calc(100% - 3.375em);
+}
+
+.widget_top-posts .widgets-list-layout li {
+ margin-bottom: 0.875em;
+}
+
+.widget_top-posts .widgets-list-layout li:last-child {
+ margin-bottom: 0;
+}
+
+.widget-grid-view-image:nth-child(odd) {
+ clear: both;
+}
+
+/* Upcoming Events Widget */
+.widget_upcoming_events_widget .upcoming-events li {
+ margin-bottom: 0.875em
+}
+
+
+/**
+ * Shortcodes
+ */
+
+/* Contact Form */
+.entry-content .contact-form label {
+ color: inherit;
+ display: block;
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+ font-size: 13px;
+ font-size: 0.8125rem;
+ font-weight: 400;
+ letter-spacing: 0.076923077em;
+ line-height: 1.6153846154;
+ margin-bottom: 0.5384615385em;
+ text-transform: uppercase;
+}
+
+.entry-content .contact-form label span {
+ color: inherit;
+ letter-spacing: 0;
+ opacity: 0.7;
+ text-transform: capitalize;
+}
+
+.entry-content .contact-form input[type="text"],
+.entry-content .contact-form input[type="email"],
+.entry-content .contact-form textarea {
+ margin-bottom: 1.75em;
+ max-width: 100%;
+ width: 100%;
+}
+
+.entry-content .contact-form label.checkbox,
+.entry-content .contact-form label.radio {
+ font-family: inherit;
+ font-size: inherit;
+ letter-spacing: 0;
+ margin-bottom: 0;
+ text-transform: none;
+}
+
+.entry-content .contact-form label.checkbox:nth-last-child(2),
+.entry-content .contact-form label.radio:nth-last-child(2) {
+ margin-bottom: 0.875em;
+}
+
+.entry-content .contact-form input[type="radio"],
+.entry-content .contact-form input[type="checkbox"] {
+ margin-bottom: 0.875em;
+}
+
+.entry-content .contact-form select {
+ margin-bottom: 1.75em;
+}
+
+/* Facebook */
+.fb_iframe_widget {
+ margin-bottom: 1.75em;
+ max-width: 100%;
+}
+
+.fb_iframe_widget span {
+ max-width: 100%;
+}
+
+/* Gist */
+.gist table {
+ table-layout: auto;
+}
+
+.entry-content .gist .gist-file {
+ margin-bottom: 1.75em;
+}
+
+/* Instagram */
+.instagram-media {
+ margin-bottom: 1.75em !important;
+}
+
+/* Mixclound */
+iframe[src^="http://api.mixcloud.com/"] {
+ margin-right: -8px;
+ max-width: -webkit-calc(100% + 8px);
+ max-width: calc(100% + 8px);
+}
+
+/* Crowdsignal */
+.PDS_Poll,
+.CSS_Poll {
+ display: block !important;
+ margin-bottom: 1.75em;
+}
+
+.PDS_Poll .pds-box,
+.CSS_Poll .css-box {
+ max-width: 100%;
+ width: auto;
+}
+
+/* Portfolio */
+.entry-content .portfolio-entry {
+ margin-bottom: 1.75em;
+}
+
+.entry-content .portfolio-entry-title,
+.entry-content .portfolio-entry-meta {
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+}
+
+.entry-content .portfolio-entry-title a,
+.entry-content .portfolio-entry-meta a {
+ box-shadow: none;
+}
+
+.entry-content .portfolio-entry-title a:hover,
+.entry-content .portfolio-entry-meta a:hover {
+ box-shadow: 0 1px 0 0 currentColor;
+}
+
+.entry-content .portfolio-entry-meta span,
+.entry-content .portfolio-entry-meta a {
+ font-size: 1em;
+}
+
+.entry-content .portfolio-entry-title {
+ font-size: inherit;
+ line-height: 1.3125;
+}
+
+.entry-content .portfolio-featured-image + .portfolio-entry-title {
+ margin-top: 0.875em;
+ margin-bottom: 0.4375em;
+}
+
+.entry-content .portfolio-entry-meta,
+.entry-content .portfolio-entry-content p {
+ font-size: 13px;
+ font-size: 0.8125em;
+ line-height: 1.615384615;
+}
+
+.entry-content .portfolio-entry-content p:last-child {
+ margin-bottom: 0;
+}
+
+.entry-content .portfolio-entry-title + .portfolio-entry-meta {
+ margin-top: 0.538461538em;
+}
+
+.entry-content .portfolio-entry-content {
+ margin-top: 0.875em;
+}
+
+.entry-content .portfolio-entry-content p {
+ margin-bottom: 1.615384615em;
+}
+
+/* Presentation */
+.entry-content .presentation-wrapper {
+ margin: 0 0 1.75em;
+ max-width: 100%;
+}
+
+.presentation {
+ max-width: 100%;
+}
+
+/* Recipes */
+.entry-content .jetpack-recipe {
+ border: 0;
+ margin: 0 0 1.75em;
+ padding: 0;
+}
+
+.entry-content .jetpack-recipe-title {
+ border: 0;
+ margin-top: 0;
+ padding: 0;
+}
+
+.entry-content .jetpack-recipe .jetpack-recipe-meta {
+ font-size: inherit;
+ margin: 0;
+}
+
+/* Scribd */
+.scribd_iframe_embed + div {
+ margin-bottom: 28px;
+}
+
+/* Slideshow */
+.entry-content .slideshow-window {
+ border-radius: 0;
+ margin-bottom: 1.75em;
+}
+
+/* Subscription Form */
+.entry-content .jetpack_subscription_widget {
+ border-top: 0;
+ font-size: inherit;
+ margin-bottom: 1.75em;
+ padding: 0;
+}
+
+.entry-content #subscribe-email input {
+ font-size: inherit;
+ line-height: normal;
+ padding: 0.625em 0.4375em;
+ width: 100%;
+}
+
+.entry-content .jetpack_subscription_widget input[type="submit"] {
+ font-size: inherit;
+ padding: 0.84375em 0.875em 0.78125em;
+}
+
+/* Testimonial */
+.entry-content .testimonial-entry {
+ margin-bottom: 1.75em;
+}
+
+.entry-content .testimonial-entry-content {
+ margin: 0;
+}
+
+.entry-content .testimonial-entry-title,
+.entry-content .testimonial-entry-content p {
+ font-size: 13px;
+ font-size: 0.8125em;
+ line-height: 1.615384615;
+ margin: 0;
+}
+
+.entry-content .testimonial-entry-content p {
+ margin-bottom: 1.615384615em;
+}
+
+.entry-content .testimonial-entry-title {
+ float: right;
+}
+
+.entry-content .testimonial-entry-title a {
+ box-shadow: none;
+}
+
+.entry-content .testimonial-entry-title a:hover {
+ box-shadow: 0 1px 0 0 currentColor;
+}
+
+.entry-content .testimonial-featured-image img {
+ float: left;
+ max-width: 42px;
+}
+
+/* Twitter-timeline */
+.twitter-timeline {
+ margin-bottom: 1.75em !important;
+}
+
+
+/**
+ * Responsive Videos
+ */
+.hentry .jetpack-video-wrapper {
+ margin-bottom: 1.75em;
+}
+
+
+/**
+ * Related Posts
+ */
+
+.entry-content #jp-relatedposts {
+ margin: 0;
+ padding: 1.75em 0;
+ position: relative;
+}
+
+.jp-relatedposts:before,
+.jp-relatedposts:last-child:after {
+ background-color: currentColor;
+ content: "";
+ height: 1px;
+ opacity: 0.2;
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 100%;
+}
+
+.jp-relatedposts:last-child:after {
+ bottom: 0;
+ top: auto;
+}
+
+.entry-content #jp-relatedposts h3.jp-relatedposts-headline {
+ color: inherit;
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+ font-size: 13px;
+ font-size: 0.8125rem;
+ font-weight: 400;
+ line-height: 1;
+ margin-bottom: 1.076923077em;
+}
+
+.entry-content #jp-relatedposts h3.jp-relatedposts-headline em:before {
+ display: none;
+}
+
+.entry-content #jp-relatedposts h3.jp-relatedposts-headline em {
+ font-weight: 400;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-headline em:after {
+ content: ":";
+}
+
+.jp-relatedposts-items:before,
+.jp-relatedposts-items:after {
+ content: "";
+ display: table;
+}
+
+.jp-relatedposts-items:after {
+ clear: both;
+}
+
+.entry-content .jp-relatedposts-post-aoverlay,
+.entry-content .jp-relatedposts-post-a {
+ box-shadow: none;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post {
+ margin-bottom: 1.75em;
+ width: 100%;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:last-child {
+ margin-bottom: 0;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post span {
+ max-width: 100%;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items p,
+.entry-content #jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title {
+ font-size: 13px;
+ font-size: 0.8125rem;
+ letter-spacing: 0;
+ line-height: 1.615384615;
+}
+
+.jp-relatedposts-post-date,
+.jp-relatedposts-post-context {
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+}
+
+.jp-relatedposts-post-title,
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post-excerpt,
+.entry-content #jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title {
+ margin-bottom: 1.076923077em;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a {
+ font-weight: 700;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a:hover,
+.entry-content .jp-relatedposts-post-aoverlay:hover + .jp-relatedposts-post-title .jp-relatedposts-post-a {
+ box-shadow: 0 1px 0 0 currentColor;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a:hover,
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:hover .jp-relatedposts-post-title a {
+ text-decoration: none;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-date,
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-context,
+.entry-content #jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post {
+ opacity: 1;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post {
+ padding-left: 0;
+}
+
+.entry-content .jp-relatedposts-post-a:hover img.jp-relatedposts-post-img,
+.entry-content .jp-relatedposts-post-a:focus img.jp-relatedposts-post-img {
+ opacity: 0.85;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post img.jp-relatedposts-post-img {
+ margin-bottom: 0.875em;
+ max-width: 100%;
+}
+
+
+/**
+ * Sharing
+ */
+
+.sharedaddy {
+ padding: 1.75em 0;
+ position: relative;
+}
+
+.sharedaddy:before,
+.sharedaddy:last-child:after {
+ background-color: currentColor;
+ content: "";
+ height: 1px;
+ opacity: 0.2;
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+
+.sharedaddy:last-child:after {
+ bottom: 0;
+ top: auto;
+}
+
+.sd-block {
+ line-height: 1;
+}
+
+.sd-like {
+ padding-bottom: 1.125em;
+}
+
+.hentry div.sharedaddy h3.sd-title,
+.hentry h3.sd-title {
+ color: inherit;
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+ font-size: 13px;
+ font-size: 0.8125rem;
+ font-weight: 400;
+ line-height: 1;
+ margin-bottom: 1.076923077em;
+}
+
+.hentry div.sharedaddy h3.sd-title:before {
+ display: none;
+}
+
+.sd-social-icon-text .sd-content ul,
+.sd-social-text .sd-content ul {
+ margin-bottom: -0.3125em !important;
+}
+
+.sd-social-icon .sd-content ul {
+ margin-bottom: 0 !important;
+}
+
+.sd-social-official .sd-content ul {
+ margin-bottom: -0.625em !important;
+}
+
+.hentry #sharing_email .sharing_send,
+.hentry .sd-content ul li .option a.share-ustom,
+.hentry .sd-content ul li a.sd-button,
+.hentry .sd-content ul li.advanced a.share-more,
+.hentry .sd-content ul li.preview-item div.option.option-smart-off a,
+.hentry .sd-social-icon .sd-content ul li a.sd-button,
+.hentry .sd-social-icon-text .sd-content ul li a.sd-button,
+.hentry .sd-social-official .sd-content > ul > li .digg_button > a,
+.hentry .sd-social-official .sd-content > ul > li > a.sd-button,
+.hentry .sd-social-text .sd-content ul li a.sd-button {
+ box-shadow: none;
+}
+
+
+/**
+ * Stats
+ */
+
+#wpstats {
+ display: none;
+}
+
+
+/**
+ * Tiled gallery
+ */
+
+.entry-content .tiled-gallery {
+ margin-bottom: 1.75em;
+}
+
+
+/**
+ * Media Queries
+ */
+
+@media screen and (min-width: 56.875em) {
+ .widget-area .jetpack_subscription_widget #subscribe-email input {
+ padding: 0.4615384615em 0.5384615385em;
+ }
+
+ .widget_contact_info .contact-map {
+ margin-bottom: 1.615384615em;
+ }
+
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4,
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts img {
+ margin-bottom: 1.076923077em;
+ }
+
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ line-height: 1.615384615em !important;
+ margin: 0 0 1.615384615em !important;
+ }
+
+ .widget_goodreads div[class^="gr_custom_each_container"] {
+ margin-bottom: 1.615384615em;
+ }
+
+ .widget-area .widget-grofile h4 {
+ margin: 1.615384615em 0 0;
+ }
+
+ .widget-area .widget-grofile .grofile-accounts {
+ margin-top: 0.538461538em;
+ }
+
+ .widget.widget_wpcom_social_media_icons_widget ul {
+ margin: 0 0 -0.538461538em;
+ }
+
+ .widget.widget_wpcom_social_media_icons_widget li {
+ margin: 0 0 0.538461538em 0.538461538em;
+ }
+
+ .widget_top-posts .widgets-list-layout .widgets-list-layout-blavatar {
+ margin-top: 0.153846154em;
+ }
+
+ .widget_top-posts .widgets-list-layout-links {
+ width: -webkit-calc(100% - 4.153846154em);
+ width: calc(100% - 4.153846154em);
+ }
+
+ .widget_top-posts .widgets-list-layout li {
+ margin-bottom: 1.076923077em;
+ }
+
+ .widget_upcoming_events_widget .upcoming-events li {
+ margin-bottom: 1.076923077em
+ }
+
+ .entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post {
+ margin-bottom: 0;
+ padding-left: 0.875em;
+ width: 33%;
+ }
+}
+
+@media screen and (min-width: 61.5625em) {
+ body:not(.search-results) article:not(.type-page) .sharedaddy:last-child,
+ body:not(.search-results) article:not(.type-page) .jp-relatedposts:last-child {
+ padding-bottom: 0;
+ }
+
+ body:not(.search-results) article:not(.type-page) .sharedaddy:last-child:after,
+ body:not(.search-results) article:not(.type-page) .jp-relatedposts:last-child:after {
+ display: none;
+ }
+
+ body:not(.search-results) article:not(.type-page) img.below-entry-meta {
+ width: auto;
+ }
+}
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentysixteen.css b/plugins/jetpack/modules/theme-tools/compat/twentysixteen.css
new file mode 100644
index 00000000..acc056cf
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentysixteen.css
@@ -0,0 +1,827 @@
+/**
+ * Jetpack Comments
+ */
+
+.comment-form iframe {
+ margin: 0;
+}
+
+.comment-form .subscribe-label {
+ font-weight: 400;
+ letter-spacing: 0;
+ text-transform: none;
+}
+
+.comment-subscription-form {
+ margin: 1.75em 0 0;
+}
+
+.comment-subscription-form + .comment-subscription-form {
+ margin-top: 0;
+}
+
+
+/**
+ * Extra Widgets
+ */
+
+ /* Blog Subscriptions Widget */
+.jetpack_subscription_widget #subscribe-email input {
+ padding: 0.625em 0.4375em;
+ width: 100%;
+}
+
+.jetpack_subscription_widget form > :last-child {
+ margin-bottom: 0;
+}
+
+ /* Contact Info Widget */
+.widget_contact_info .contact-map {
+ margin-bottom: 1.75em;
+}
+
+/* Display WordPress Posts Widget */
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts {
+ margin: 0;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4 {
+ font-size: inherit;
+ margin: 0 0 0.875em;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts img {
+ margin-bottom: 0.875em;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ font-size: inherit;
+ line-height: 1.75 !important;
+ margin: 0 0 1.75em !important;
+}
+
+.widget_jetpack_display_posts_widget .jetpack-display-remote-posts > :last-child {
+ margin-bottom: 0 !important;
+}
+
+/* Gallery Widget */
+.widget-gallery .slideshow-window {
+ border-radius: 0;
+}
+
+/* Goodreads Widget */
+.widget_goodreads div[class^="gr_custom_container"] {
+ background-color: transparent;
+ border: 0;
+ border-radius: 0;
+ color: inherit;
+ padding: 0;
+}
+
+.widget_goodreads div[class^="gr_custom_container"] a {
+ color: inherit;
+}
+
+.widget_goodreads div[class^="gr_custom_each_container"] {
+ border: 0;
+ margin-bottom: 1.75em;
+ padding-bottom: 0;
+}
+
+.widget_goodreads h2[class^="gr_custom_header"],
+.widget_goodreads div[class^="gr_custom_author"] {
+ font-size: inherit;
+}
+
+/* Gravatar Profile Widget */
+.widget-grofile .grofile-thumbnail {
+ width: 300px;
+}
+
+.widget-area .widget-grofile h4 {
+ font-size: inherit;
+ font-weight: 900;
+ margin: 1.75em 0 0;
+}
+
+.widget-area .widget-grofile .grofile-accounts {
+ margin-top: 0.4375em;
+}
+
+/* Image Widget */
+.widget_image .wp-caption {
+ margin-bottom: 0;
+}
+
+/* RSS Links Widget */
+.widget_rss_links img {
+ position: relative;
+ top: -1px;
+}
+
+/* Social Media Icon Widget */
+.widget.widget_wpcom_social_media_icons_widget ul {
+ margin: 0 0 -0.4375em;
+}
+
+.widget.widget_wpcom_social_media_icons_widget ul:before,
+.widget.widget_wpcom_social_media_icons_widget ul:after {
+ content: "";
+ display: table;
+}
+
+.widget.widget_wpcom_social_media_icons_widget ul:after {
+ clear: both;
+}
+
+.widget.widget_wpcom_social_media_icons_widget li {
+ float: left;
+ margin: 0 0.4375em 0.4375em 0;
+}
+
+.widget.widget_wpcom_social_media_icons_widget li a {
+ border: 1px solid currentColor;
+ border-radius: 50%;
+ color: inherit;
+ display: block;
+ height: 35px;
+ position: relative;
+ width: 35px;
+}
+
+.widget.widget_wpcom_social_media_icons_widget li a:before {
+ height: 33px;
+ line-height: 33px;
+ text-align: center;
+ width: 33px;
+}
+
+.widget.widget_wpcom_social_media_icons_widget li a:hover:before,
+.widget.widget_wpcom_social_media_icons_widget li a:focus:before {
+ opacity: 0.8;
+}
+
+/* Social Icons Widget */
+.widget.jetpack_widget_social_icons ul {
+ margin: 0 0 -0.4375em;
+}
+
+.widget.jetpack_widget_social_icons ul:before,
+.widget.jetpack_widget_social_icons ul:after {
+ content: "";
+ display: table;
+}
+
+.widget.jetpack_widget_social_icons ul:after {
+ clear: both;
+}
+
+.widget.jetpack_widget_social_icons li {
+ float: left;
+ margin: 0 0.4375em 0.4375em 0;
+}
+
+.widget.jetpack_widget_social_icons li a {
+ border: 1px solid currentColor;
+ border-radius: 50%;
+ color: inherit;
+ display: block;
+ position: relative;
+}
+
+.widget.jetpack_widget_social_icons li a:hover,
+.widget.jetpack_widget_social_icons li a:focus {
+ opacity: 0.8;
+}
+
+.widget.jetpack_widget_social_icons ul.size-small a {
+ height: 38px;
+ padding: 6px;
+ width: 38px;
+}
+
+.widget.jetpack_widget_social_icons ul.size-small svg {
+ height: 24px;
+ width: 24px;
+}
+
+.widget.jetpack_widget_social_icons ul.size-medium a {
+ height: 50px;
+ padding: 8px;
+ width: 50px;
+}
+
+.widget.jetpack_widget_social_icons ul.size-medium svg {
+ height: 32px;
+ width: 32px;
+}
+
+.widget.jetpack_widget_social_icons ul.size-large a {
+ height: 70px;
+ padding: 10px;
+ width: 70px;
+}
+
+.widget.jetpack_widget_social_icons ul.size-large svg {
+ height: 48px;
+ width: 48px;
+}
+
+/* Top Posts & Pages Widget */
+.widget_top-posts .widgets-list-layout .widgets-list-layout-blavatar {
+ margin-top: 0.25em;
+}
+
+.widget_top-posts .widgets-list-layout-links {
+ width: -webkit-calc(100% - 3.375em);
+ width: calc(100% - 3.375em);
+}
+
+.widget_top-posts .widgets-list-layout li {
+ margin-bottom: 0.875em;
+}
+
+.widget_top-posts .widgets-list-layout li:last-child {
+ margin-bottom: 0;
+}
+
+.widget-grid-view-image:nth-child(odd) {
+ clear: both;
+}
+
+/* Upcoming Events Widget */
+.widget_upcoming_events_widget .upcoming-events li {
+ margin-bottom: 0.875em
+}
+
+/**
+ * Shortcodes
+ */
+
+/* Contact Form */
+.entry-content .contact-form label {
+ color: inherit;
+ display: block;
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+ font-size: 13px;
+ font-size: 0.8125rem;
+ font-weight: 400;
+ letter-spacing: 0.076923077em;
+ line-height: 1.6153846154;
+ margin-bottom: 0.5384615385em;
+ text-transform: uppercase;
+}
+
+.entry-content .contact-form label span {
+ color: inherit;
+ letter-spacing: 0;
+ opacity: 0.7;
+ text-transform: capitalize;
+}
+
+.entry-content .contact-form input[type="text"],
+.entry-content .contact-form input[type="email"],
+.entry-content .contact-form textarea {
+ margin-bottom: 1.75em;
+ max-width: 100%;
+ width: 100%;
+}
+
+.entry-content .contact-form label.checkbox,
+.entry-content .contact-form label.radio {
+ font-family: inherit;
+ font-size: inherit;
+ letter-spacing: 0;
+ margin-bottom: 0;
+ text-transform: none;
+}
+
+.entry-content .contact-form label.checkbox:nth-last-child(2),
+.entry-content .contact-form label.radio:nth-last-child(2) {
+ margin-bottom: 0.875em;
+}
+
+.entry-content .contact-form input[type="radio"],
+.entry-content .contact-form input[type="checkbox"] {
+ margin-bottom: 0.875em;
+}
+
+.entry-content .contact-form select {
+ margin-bottom: 1.75em;
+}
+
+/* Facebook */
+.fb_iframe_widget {
+ margin-bottom: 1.75em;
+ max-width: 100%;
+}
+
+.fb_iframe_widget span {
+ max-width: 100%;
+}
+
+/* Gist */
+.gist table {
+ table-layout: auto;
+}
+
+.entry-content .gist .gist-file {
+ margin-bottom: 1.75em;
+}
+
+/* Instagram */
+.instagram-media {
+ margin-bottom: 1.75em !important;
+}
+
+/* Mixclound */
+iframe[src^="http://api.mixcloud.com/"] {
+ margin-left: -8px;
+ max-width: -webkit-calc(100% + 8px);
+ max-width: calc(100% + 8px);
+}
+
+/* Crowdsignal */
+.PDS_Poll,
+.CSS_Poll {
+ display: block !important;
+ margin-bottom: 1.75em;
+}
+
+.PDS_Poll .pds-box,
+.CSS_Poll .css-box {
+ max-width: 100%;
+ width: auto;
+}
+
+/* Portfolio */
+.entry-content .portfolio-entry {
+ margin-bottom: 1.75em;
+}
+
+.entry-content .portfolio-entry-title,
+.entry-content .portfolio-entry-meta {
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+}
+
+.entry-content .portfolio-entry-title a,
+.entry-content .portfolio-entry-meta a {
+ box-shadow: none;
+}
+
+.entry-content .portfolio-entry-title a:hover,
+.entry-content .portfolio-entry-meta a:hover {
+ box-shadow: 0 1px 0 0 currentColor;
+}
+
+.entry-content .portfolio-entry-meta span,
+.entry-content .portfolio-entry-meta a {
+ font-size: 1em;
+}
+
+.entry-content .portfolio-entry-title {
+ font-size: inherit;
+ line-height: 1.3125;
+}
+
+.entry-content .portfolio-featured-image + .portfolio-entry-title {
+ margin-top: 0.875em;
+ margin-bottom: 0.4375em;
+}
+
+.entry-content .portfolio-entry-meta,
+.entry-content .portfolio-entry-content p {
+ font-size: 13px;
+ font-size: 0.8125em;
+ line-height: 1.615384615;
+}
+
+.entry-content .portfolio-entry-content p:last-child {
+ margin-bottom: 0;
+}
+
+.entry-content .portfolio-entry-title + .portfolio-entry-meta {
+ margin-top: 0.538461538em;
+}
+
+.entry-content .portfolio-entry-content {
+ margin-top: 0.875em;
+}
+
+.entry-content .portfolio-entry-content p {
+ margin-bottom: 1.615384615em;
+}
+
+/* Presentation */
+.entry-content .presentation-wrapper {
+ margin: 0 0 1.75em;
+ max-width: 100%;
+}
+
+.presentation {
+ max-width: 100%;
+}
+
+/* Recipes */
+.entry-content .jetpack-recipe {
+ border: 0;
+ margin: 0 0 1.75em;
+ padding: 0;
+}
+
+.entry-content .jetpack-recipe-title {
+ border: 0;
+ margin-top: 0;
+ padding: 0;
+}
+
+.entry-content .jetpack-recipe .jetpack-recipe-meta {
+ font-size: inherit;
+ margin: 0;
+}
+
+/* Scribd */
+.scribd_iframe_embed + div {
+ margin-bottom: 28px;
+}
+
+/* Slideshow */
+.entry-content .slideshow-window {
+ border-radius: 0;
+ margin-bottom: 1.75em;
+}
+
+/* Subscription Form */
+.entry-content .jetpack_subscription_widget {
+ border-top: 0;
+ font-size: inherit;
+ margin-bottom: 1.75em;
+ padding: 0;
+}
+
+.entry-content #subscribe-email input {
+ font-size: inherit;
+ line-height: normal;
+ padding: 0.625em 0.4375em;
+ width: 100%;
+}
+
+.entry-content .jetpack_subscription_widget input[type="submit"] {
+ font-size: inherit;
+ padding: 0.84375em 0.875em 0.78125em;
+}
+
+/* Testimonial */
+.entry-content .testimonial-entry {
+ margin-bottom: 1.75em;
+}
+
+.entry-content .testimonial-entry-content {
+ margin: 0;
+}
+
+.entry-content .testimonial-entry-title,
+.entry-content .testimonial-entry-content p {
+ font-size: 13px;
+ font-size: 0.8125em;
+ line-height: 1.615384615;
+ margin: 0;
+}
+
+.entry-content .testimonial-entry-content p {
+ margin-bottom: 1.615384615em;
+}
+
+.entry-content .testimonial-entry-title {
+ float: left;
+}
+
+.entry-content .testimonial-entry-title a {
+ box-shadow: none;
+}
+
+.entry-content .testimonial-entry-title a:hover {
+ box-shadow: 0 1px 0 0 currentColor;
+}
+
+.entry-content .testimonial-featured-image img {
+ float: right;
+ max-width: 42px;
+}
+
+/* Twitter-timeline */
+.twitter-timeline {
+ margin-bottom: 1.75em !important;
+}
+
+
+/**
+ * Responsive Videos
+ */
+.hentry .jetpack-video-wrapper {
+ margin-bottom: 1.75em;
+}
+
+
+/**
+ * Related Posts
+ */
+
+.entry-content #jp-relatedposts {
+ margin: 0;
+ padding: 1.75em 0;
+ position: relative;
+}
+
+.jp-relatedposts:before,
+.jp-relatedposts:last-child:after {
+ background-color: currentColor;
+ content: "";
+ height: 1px;
+ opacity: 0.2;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+}
+
+.jp-relatedposts:last-child:after {
+ bottom: 0;
+ top: auto;
+}
+
+.entry-content #jp-relatedposts h3.jp-relatedposts-headline {
+ color: inherit;
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+ font-size: 13px;
+ font-size: 0.8125rem;
+ font-weight: 400;
+ line-height: 1;
+ margin-bottom: 1.076923077em;
+}
+
+.entry-content #jp-relatedposts h3.jp-relatedposts-headline em:before {
+ display: none;
+}
+
+.entry-content #jp-relatedposts h3.jp-relatedposts-headline em {
+ font-weight: 400;
+}
+
+.jp-relatedposts-items:before,
+.jp-relatedposts-items:after {
+ content: "";
+ display: table;
+}
+
+.jp-relatedposts-items:after {
+ clear: both;
+}
+
+.entry-content .jp-relatedposts-post-aoverlay,
+.entry-content .jp-relatedposts-post-a {
+ box-shadow: none;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post {
+ margin-bottom: 1.75em;
+ width: 100%;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:last-child {
+ margin-bottom: 0;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post span {
+ max-width: 100%;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items p,
+.entry-content #jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title {
+ font-size: 13px;
+ font-size: 0.8125rem;
+ letter-spacing: 0;
+ line-height: 1.615384615;
+}
+
+.jp-relatedposts-post-date,
+.jp-relatedposts-post-context {
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+}
+
+.jp-relatedposts-post-title,
+#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post-excerpt,
+.entry-content #jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title {
+ margin-bottom: 1.076923077em;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a {
+ font-weight: 700;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a:hover,
+.entry-content .jp-relatedposts-post-aoverlay:hover + .jp-relatedposts-post-title .jp-relatedposts-post-a {
+ box-shadow: 0 1px 0 0 currentColor;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a:hover,
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post:hover .jp-relatedposts-post-title a {
+ text-decoration: none;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-date,
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-context,
+.entry-content #jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post {
+ opacity: 1;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post {
+ padding-right: 0;
+}
+
+.entry-content .jp-relatedposts-post-a:hover img.jp-relatedposts-post-img,
+.entry-content .jp-relatedposts-post-a:focus img.jp-relatedposts-post-img {
+ opacity: 0.85;
+}
+
+.entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post img.jp-relatedposts-post-img {
+ margin-bottom: 0.875em;
+ max-width: 100%;
+}
+
+
+/**
+ * Sharing
+ */
+
+.sharedaddy {
+ padding: 1.75em 0;
+ position: relative;
+}
+
+.sharedaddy:before,
+.sharedaddy:last-child:after {
+ background-color: currentColor;
+ content: "";
+ height: 1px;
+ opacity: 0.2;
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+
+.sharedaddy:last-child:after {
+ bottom: 0;
+ top: auto;
+}
+
+.sd-block {
+ line-height: 1;
+}
+
+.sd-like {
+ padding-bottom: 1.125em;
+}
+
+.hentry div.sharedaddy h3.sd-title,
+.hentry h3.sd-title {
+ color: inherit;
+ font-family: Montserrat, "Helvetica Neue", sans-serif;
+ font-size: 13px;
+ font-size: 0.8125rem;
+ font-weight: 400;
+ line-height: 1;
+ margin-bottom: 1.076923077em;
+}
+
+.hentry div.sharedaddy h3.sd-title:before {
+ display: none;
+}
+
+.sd-social-icon-text .sd-content ul,
+.sd-social-text .sd-content ul {
+ margin-bottom: -0.3125em !important;
+}
+
+.sd-social-icon .sd-content ul {
+ margin-bottom: 0 !important;
+}
+
+.sd-social-official .sd-content ul {
+ margin-bottom: -0.625em !important;
+}
+
+.hentry #sharing_email .sharing_send,
+.hentry .sd-content ul li .option a.share-ustom,
+.hentry .sd-content ul li a.sd-button,
+.hentry .sd-content ul li.advanced a.share-more,
+.hentry .sd-content ul li.preview-item div.option.option-smart-off a,
+.hentry .sd-social-icon .sd-content ul li a.sd-button,
+.hentry .sd-social-icon-text .sd-content ul li a.sd-button,
+.hentry .sd-social-official .sd-content > ul > li .digg_button > a,
+.hentry .sd-social-official .sd-content > ul > li > a.sd-button,
+.hentry .sd-social-text .sd-content ul li a.sd-button {
+ box-shadow: none;
+}
+
+
+/**
+ * Stats
+ */
+
+#wpstats {
+ display: none;
+}
+
+
+/**
+ * Tiled gallery
+ */
+
+.entry-content .tiled-gallery {
+ margin-bottom: 1.75em;
+}
+
+
+/**
+ * Media Queries
+ */
+
+@media screen and (min-width: 56.875em) {
+ .widget-area .jetpack_subscription_widget #subscribe-email input {
+ padding: 0.4615384615em 0.5384615385em;
+ }
+
+ .widget_contact_info .contact-map {
+ margin-bottom: 1.615384615em;
+ }
+
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts h4,
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts img {
+ margin-bottom: 1.076923077em;
+ }
+
+ .widget_jetpack_display_posts_widget .jetpack-display-remote-posts p {
+ line-height: 1.615384615em !important;
+ margin: 0 0 1.615384615em !important;
+ }
+
+ .widget_goodreads div[class^="gr_custom_each_container"] {
+ margin-bottom: 1.615384615em;
+ }
+
+ .widget-area .widget-grofile h4 {
+ margin: 1.615384615em 0 0;
+ }
+
+ .widget-area .widget-grofile .grofile-accounts {
+ margin-top: 0.538461538em;
+ }
+
+ .widget.widget_wpcom_social_media_icons_widget ul {
+ margin: 0 0 -0.538461538em;
+ }
+
+ .widget.widget_wpcom_social_media_icons_widget li {
+ margin: 0 0.538461538em 0.538461538em 0;
+ }
+
+ .widget_top-posts .widgets-list-layout .widgets-list-layout-blavatar {
+ margin-top: 0.153846154em;
+ }
+
+ .widget_top-posts .widgets-list-layout-links {
+ width: -webkit-calc(100% - 4.153846154em);
+ width: calc(100% - 4.153846154em);
+ }
+
+ .widget_top-posts .widgets-list-layout li {
+ margin-bottom: 1.076923077em;
+ }
+
+ .widget_upcoming_events_widget .upcoming-events li {
+ margin-bottom: 1.076923077em
+ }
+
+ .entry-content #jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post {
+ margin-bottom: 0;
+ padding-right: 0.875em;
+ width: 33%;
+ }
+}
+
+@media screen and (min-width: 61.5625em) {
+ body:not(.search-results) article:not(.type-page) .sharedaddy:last-child,
+ body:not(.search-results) article:not(.type-page) .jp-relatedposts:last-child {
+ padding-bottom: 0;
+ }
+
+ body:not(.search-results) article:not(.type-page) .sharedaddy:last-child:after,
+ body:not(.search-results) article:not(.type-page) .jp-relatedposts:last-child:after {
+ display: none;
+ }
+
+ body:not(.search-results) article:not(.type-page) img.below-entry-meta {
+ width: auto;
+ }
+}
diff --git a/plugins/jetpack/modules/theme-tools/compat/twentysixteen.php b/plugins/jetpack/modules/theme-tools/compat/twentysixteen.php
new file mode 100644
index 00000000..816a74b5
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/compat/twentysixteen.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Jetpack Compatibility File
+ * See: http://jetpack.com/
+ */
+
+function twentysixteen_jetpack_setup() {
+ /**
+ * Add theme support for Responsive Videos.
+ */
+ add_theme_support( 'jetpack-responsive-videos' );
+
+ /**
+ * Add theme support for geo-location.
+ */
+ add_theme_support( 'jetpack-geo-location' );
+}
+add_action( 'after_setup_theme', 'twentysixteen_jetpack_setup' );
+
+function twentysixteen_init_jetpack() {
+ /**
+ * Add our compat CSS file for custom widget stylings and such.
+ * Set the version equal to filemtime for development builds, and the JETPACK__VERSION for production
+ * or skip it entirely for wpcom.
+ */
+ if ( ! is_admin() ) {
+ $version = false;
+ if ( method_exists( 'Jetpack', 'is_development_version' ) ) {
+ $version = Jetpack::is_development_version() ? filemtime( plugin_dir_path( __FILE__ ) . 'twentysixteen.css' ) : JETPACK__VERSION;
+ }
+ wp_enqueue_style( 'twentysixteen-jetpack', plugins_url( 'twentysixteen.css', __FILE__ ), array(), $version );
+ wp_style_add_data( 'twentysixteen-jetpack', 'rtl', 'replace' );
+ }
+}
+add_action( 'init', 'twentysixteen_init_jetpack' );
+
+/**
+ * Alter gallery widget default width.
+ */
+function twentysixteen_gallery_widget_content_width( $width ) {
+ return 390;
+}
+add_filter( 'gallery_widget_content_width', 'twentysixteen_gallery_widget_content_width' );
+
+/**
+ * Remove ratings from excerpts that are used as intro on blog index, single, and archive pages.
+ */
+function twentysixteen_remove_share() {
+ if ( is_single() || is_archive() || is_home() ) {
+ remove_filter( 'the_excerpt', 'sharing_display', 19 );
+ if ( class_exists( 'Jetpack_Likes' ) ) {
+ remove_filter( 'the_excerpt', array( Jetpack_Likes::init(), 'post_likes' ), 30, 1 );
+ }
+ }
+}
+add_action( 'loop_start', 'twentysixteen_remove_share' );
+
+function twentysixteen_jetpack_lazy_images_compat() {
+ // Since TwentySixteen outdents when window is resized, let's trigger a window resize
+ // every time we lazy load an image on the TwentySixteen theme.
+ wp_add_inline_script(
+ 'jetpack-lazy-images',
+ "jQuery( document.body ).on( 'jetpack-lazy-loaded-image', function () { jQuery( window ).trigger( 'resize' ); } );"
+ );
+}
+
+// Priority needs to be 11 here so that we have already enqueued jetpack-lazy-images.
+add_action( 'wp_enqueue_scripts', 'twentysixteen_jetpack_lazy_images_compat', 11 );
diff --git a/plugins/jetpack/modules/theme-tools/content-options.php b/plugins/jetpack/modules/theme-tools/content-options.php
new file mode 100644
index 00000000..118e31db
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/content-options.php
@@ -0,0 +1,137 @@
+<?php
+/**
+ * Content Options.
+ *
+ * This feature will only be activated for themes that declare their support.
+ * This can be done by adding code similar to the following during the
+ * 'after_setup_theme' action:
+ *
+ add_theme_support( 'jetpack-content-options', array(
+ 'blog-display' => 'content', // the default setting of the theme: 'content', 'excerpt' or array( 'content', 'excerpt' ) for themes mixing both display.
+ 'author-bio' => true, // display or not the author bio: true or false.
+ 'author-bio-default' => false, // the default setting of the author bio, if it's being displayed or not: true or false (only required if false).
+ 'masonry' => '.site-main', // a CSS selector matching the elements that triggers a masonry refresh if the theme is using a masonry layout.
+ 'post-details' => array(
+ 'stylesheet' => 'themeslug-style', // name of the theme's stylesheet.
+ 'date' => '.posted-on', // a CSS selector matching the elements that display the post date.
+ 'categories' => '.cat-links', // a CSS selector matching the elements that display the post categories.
+ 'tags' => '.tags-links', // a CSS selector matching the elements that display the post tags.
+ 'author' => '.byline', // a CSS selector matching the elements that display the post author.
+ 'comment' => '.comments-link', // a CSS selector matching the elements that display the comment link.
+ ),
+ 'featured-images' => array(
+ 'archive' => true, // enable or not the featured image check for archive pages: true or false.
+ 'archive-default' => false, // the default setting of the featured image on archive pages, if it's being displayed or not: true or false (only required if false).
+ 'post' => true, // enable or not the featured image check for single posts: true or false.
+ 'post-default' => false, // the default setting of the featured image on single posts, if it's being displayed or not: true or false (only required if false).
+ 'page' => true, // enable or not the featured image check for single pages: true or false.
+ 'page-default' => false, // the default setting of the featured image on single pages, if it's being displayed or not: true or false (only required if false).
+ 'portfolio' => true, // enable or not the featured image check for single projects: true or false.
+ 'portfolio-default' => false, // the default setting of the featured image on single projects, if it's being displayed or not: true or false (only required if false).
+ 'fallback' => true, // enable or not the featured image fallback: true or false.
+ 'fallback-default' => true, // the default setting for featured image fallbacks: true or false (only required if false)
+ ),
+ ) );
+ */
+
+/**
+ * Activate the Content Options plugin.
+ *
+ * @uses current_theme_supports()
+ */
+function jetpack_content_options_init() {
+ // If the theme doesn't support 'jetpack-content-options', don't continue.
+ if ( ! current_theme_supports( 'jetpack-content-options' ) ) {
+ return;
+ }
+
+ // Load the Customizer options.
+ require dirname( __FILE__ ) . '/content-options/customizer.php';
+
+ // Load Blog Display function.
+ require dirname( __FILE__ ) . '/content-options/blog-display.php';
+
+ // Load Author Bio function.
+ require dirname( __FILE__ ) . '/content-options/author-bio.php';
+
+ // Load Post Details function.
+ require dirname( __FILE__ ) . '/content-options/post-details.php';
+
+ // Load Featured Images function.
+ if ( jetpack_featured_images_should_load() ) {
+ require dirname( __FILE__ ) . '/content-options/featured-images.php';
+ }
+
+ // Load Featured Images Fallback function.
+ if ( jetpack_featured_images_fallback_should_load() ) {
+ require dirname( __FILE__ ) . '/content-options/featured-images-fallback.php';
+ }
+}
+add_action( 'init', 'jetpack_content_options_init' );
+
+function jetpack_featured_images_get_settings() {
+ $options = get_theme_support( 'jetpack-content-options' );
+
+ $featured_images = ( ! empty( $options[0]['featured-images'] ) ) ? $options[0]['featured-images'] : null;
+
+ $settings = array(
+ 'archive' => ( ! empty( $featured_images['archive'] ) ) ? $featured_images['archive'] : null,
+ 'post' => ( ! empty( $featured_images['post'] ) ) ? $featured_images['post'] : null,
+ 'page' => ( ! empty( $featured_images['page'] ) ) ? $featured_images['page'] : null,
+ 'portfolio' => ( ! empty( $featured_images['portfolio'] ) ) ? $featured_images['portfolio'] : null,
+ 'archive-default' => ( isset( $featured_images['archive-default'] ) && false === $featured_images['archive-default'] ) ? '' : 1,
+ 'post-default' => ( isset( $featured_images['post-default'] ) && false === $featured_images['post-default'] ) ? '' : 1,
+ 'page-default' => ( isset( $featured_images['page-default'] ) && false === $featured_images['page-default'] ) ? '' : 1,
+ 'portfolio-default' => ( isset( $featured_images['portfolio-default'] ) && false === $featured_images['portfolio-default'] ) ? '' : 1,
+ 'fallback' => ( ! empty( $featured_images['fallback'] ) ) ? $featured_images['fallback'] : null,
+ 'fallback-default' => ( isset( $featured_images['fallback-default'] ) && false === $featured_images['fallback-default'] ) ? '' : 1,
+ );
+
+ $settings = array_merge(
+ $settings,
+ array(
+ 'archive-option' => get_option( 'jetpack_content_featured_images_archive', $settings['archive-default'] ),
+ 'post-option' => get_option( 'jetpack_content_featured_images_post', $settings['post-default'] ),
+ 'page-option' => get_option( 'jetpack_content_featured_images_page', $settings['page-default'] ),
+ 'portfolio-option' => get_option( 'jetpack_content_featured_images_portfolio', $settings['portfolio-default'] ),
+ 'fallback-option' => get_option( 'jetpack_content_featured_images_fallback', $settings['fallback-default'] ),
+ )
+ );
+
+ return $settings;
+}
+
+function jetpack_featured_images_should_load() {
+ // If the theme doesn't support post thumbnails, don't continue.
+ if ( ! current_theme_supports( 'post-thumbnails' ) ) {
+ return false;
+ }
+
+ $opts = jetpack_featured_images_get_settings();
+
+ // If the theme doesn't support archive, post and page or if all the options are ticked and we aren't in the customizer, don't continue.
+ if (
+ ( true !== $opts['archive'] && true !== $opts['post'] && true !== $opts['page'] )
+ || ( 1 === $opts['archive-option'] && 1 === $opts['post-option'] && 1 === $opts['page-option'] && ! is_customize_preview() )
+ ) {
+ return false;
+ }
+
+ return true;
+}
+
+function jetpack_featured_images_fallback_should_load() {
+ // If the theme doesn't support post thumbnails, don't continue.
+ if ( ! current_theme_supports( 'post-thumbnails' ) ) {
+ return false;
+ }
+
+ $opts = jetpack_featured_images_get_settings();
+
+ // If the theme doesn't support fallback, don't continue.
+ if ( true !== $opts['fallback'] ) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/plugins/jetpack/modules/theme-tools/content-options/author-bio.php b/plugins/jetpack/modules/theme-tools/content-options/author-bio.php
new file mode 100644
index 00000000..89d4603c
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/content-options/author-bio.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * The function to display Author Bio in a theme.
+ */
+function jetpack_author_bio() {
+ // If the theme doesn't support 'jetpack-content-options', don't continue.
+ if ( ! current_theme_supports( 'jetpack-content-options' ) ) {
+ return;
+ }
+
+ $options = get_theme_support( 'jetpack-content-options' );
+ $author_bio = ( ! empty( $options[0]['author-bio'] ) ) ? $options[0]['author-bio'] : null;
+ $author_bio_default = ( isset( $options[0]['author-bio-default'] ) && false === $options[0]['author-bio-default'] ) ? '' : 1;
+
+ // If the theme doesn't support 'jetpack-content-options[ 'author-bio' ]', don't continue.
+ if ( true !== $author_bio ) {
+ return;
+ }
+
+ // If 'jetpack_content_author_bio' is false, don't continue.
+ if ( ! get_option( 'jetpack_content_author_bio', $author_bio_default ) ) {
+ return;
+ }
+
+ // If we aren't on a single post, don't continue.
+ if ( ! is_single() ) {
+ return;
+ }
+ ?>
+ <div class="entry-author">
+ <div class="author-avatar">
+ <?php
+ /**
+ * Filter the author bio avatar size.
+ *
+ * @param int $size The avatar height and width size in pixels.
+ *
+ * @module theme-tools
+ *
+ * @since 4.5.0
+ */
+ $author_bio_avatar_size = apply_filters( 'jetpack_author_bio_avatar_size', 48 );
+
+ echo get_avatar( get_the_author_meta( 'user_email' ), $author_bio_avatar_size );
+ ?>
+ </div><!-- .author-avatar -->
+
+ <div class="author-heading">
+ <h2 class="author-title"><?php printf( esc_html__( 'Published by %s', 'jetpack' ), '<span class="author-name">' . get_the_author() . '</span>' ); ?></h2>
+ </div><!-- .author-heading -->
+
+ <p class="author-bio">
+ <?php the_author_meta( 'description' ); ?>
+ <a class="author-link" href="<?php echo esc_url( get_author_posts_url( get_the_author_meta( 'ID' ) ) ); ?>" rel="author">
+ <?php printf( esc_html__( 'View all posts by %s', 'jetpack' ), get_the_author() ); ?>
+ </a>
+ </p><!-- .author-bio -->
+ </div><!-- .entry-auhtor -->
+ <?php
+}
diff --git a/plugins/jetpack/modules/theme-tools/content-options/blog-display.php b/plugins/jetpack/modules/theme-tools/content-options/blog-display.php
new file mode 100644
index 00000000..a3f74d0b
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/content-options/blog-display.php
@@ -0,0 +1,240 @@
+<?php
+/**
+ * The functions to display Content or Excerpt in a theme.
+ */
+
+/**
+ * If the theme doesn't support 'jetpack-content-options', don't continue.
+ */
+if ( ! current_theme_supports( 'jetpack-content-options' ) ) {
+ return;
+}
+
+/**
+ * Get the Blog Display setting.
+ * If theme is using both 'Content' and 'Excerpt' then this setting will be called 'Mixed'.
+ */
+$options = get_theme_support( 'jetpack-content-options' );
+$blog_display = ( ! empty( $options[0]['blog-display'] ) ) ? $options[0]['blog-display'] : null;
+$blog_display = preg_grep( '/^(content|excerpt)$/', (array) $blog_display );
+sort( $blog_display );
+$blog_display = implode( ', ', $blog_display );
+$blog_display = ( 'content, excerpt' === $blog_display ) ? 'mixed' : $blog_display;
+
+/**
+ * If the theme doesn't support 'jetpack-content-options[ 'blog-display' ]', don't continue.
+ */
+if ( ! in_array( $blog_display, array( 'content', 'excerpt', 'mixed' ) ) ) {
+ return;
+}
+
+/**
+ * Apply Content filters.
+ */
+function jetpack_blog_display_custom_excerpt( $content ) {
+ $post = get_post();
+ if ( empty( $post->post_excerpt ) ) {
+ $text = strip_shortcodes( $post->post_content );
+ $text = str_replace( ']]>', ']]&gt;', $text );
+ $text = strip_tags( $text );
+ /** This filter is documented in wp-includes/formatting.php */
+ $excerpt_length = apply_filters( 'excerpt_length', 55 );
+ /** This filter is documented in wp-includes/formatting.php */
+ $excerpt_more = apply_filters( 'excerpt_more', ' ' . '[...]' );
+
+ /*
+ * translators: If your word count is based on single characters (e.g. East Asian characters),
+ * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
+ * Do not translate into your own language.
+ */
+ if ( strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ) ) {
+ $text = trim( preg_replace( "/[\n\r\t ]+/", ' ', $text ), ' ' );
+ preg_match_all( '/./u', $text, $words );
+ $words = array_slice( $words[0], 0, $excerpt_length + 1 );
+ $sep = '';
+ } else {
+ $words = preg_split( "/[\n\r\t ]+/", $text, $excerpt_length + 1, PREG_SPLIT_NO_EMPTY );
+ $sep = ' ';
+ }
+
+ if ( count( $words ) > $excerpt_length ) {
+ array_pop( $words );
+ $text = implode( $sep, $words );
+ $text = $text . $excerpt_more;
+ } else {
+ $text = implode( $sep, $words );
+ }
+ } else {
+ $text = wp_kses_post( $post->post_excerpt );
+ }
+ return sprintf( '<p>%s</p>', $text );
+}
+
+/**
+ * Display Excerpt instead of Content.
+ */
+function jetpack_the_content_to_the_excerpt( $content ) {
+ if ( ( is_home() || is_archive() ) && ! is_post_type_archive( array( 'jetpack-testimonial', 'jetpack-portfolio', 'product' ) ) ) {
+ if ( post_password_required() ) {
+ $content = sprintf( '<p>%s</p>', esc_html__( 'There is no excerpt because this is a protected post.', 'jetpack' ) );
+ } else {
+ $content = jetpack_blog_display_custom_excerpt( $content );
+ }
+ }
+ return $content;
+}
+
+/**
+ * Display Content instead of Excerpt.
+ */
+function jetpack_the_excerpt_to_the_content( $content ) {
+ if ( ( is_home() || is_archive() ) && ! is_post_type_archive( array( 'jetpack-testimonial', 'jetpack-portfolio', 'product' ) ) ) {
+ ob_start();
+ the_content(
+ sprintf(
+ wp_kses(
+ /* translators: %s: Name of current post. Only visible to screen readers */
+ __( 'Continue reading<span class="screen-reader-text"> "%s"</span>', 'jetpack' ),
+ array(
+ 'span' => array(
+ 'class' => array(),
+ ),
+ )
+ ),
+ get_the_title()
+ )
+ );
+ $content = ob_get_clean();
+ }
+ return $content;
+}
+
+/**
+ * Display both Content and Excerpt instead of Content in the Customizer so live preview can switch between them.
+ */
+function jetpack_the_content_customizer( $content ) {
+ $class = jetpack_the_content_customizer_class();
+ if ( ( is_home() || is_archive() ) && ! is_post_type_archive( array( 'jetpack-testimonial', 'jetpack-portfolio', 'product' ) ) ) {
+ if ( post_password_required() ) {
+ $excerpt = sprintf( '<p>%s</p>', esc_html__( 'There is no excerpt because this is a protected post.', 'jetpack' ) );
+ } else {
+ $excerpt = jetpack_blog_display_custom_excerpt( $content );
+ }
+ }
+ if ( empty( $excerpt ) ) {
+ return $content;
+ } else {
+ return sprintf( '<div class="jetpack-blog-display %s jetpack-the-content">%s</div><div class="jetpack-blog-display %s jetpack-the-excerpt">%s</div>', $class, $content, $class, $excerpt );
+ }
+}
+
+/**
+ * Display both Content and Excerpt instead of Excerpt in the Customizer so live preview can switch between them.
+ */
+function jetpack_the_excerpt_customizer( $excerpt ) {
+ if ( ( is_home() || is_archive() ) && ! is_post_type_archive( array( 'jetpack-testimonial', 'jetpack-portfolio', 'product' ) ) ) {
+ ob_start();
+ the_content(
+ sprintf(
+ wp_kses(
+ /* translators: %s: Name of current post. Only visible to screen readers */
+ __( 'Continue reading<span class="screen-reader-text"> "%s"</span>', 'jetpack' ),
+ array(
+ 'span' => array(
+ 'class' => array(),
+ ),
+ )
+ ),
+ get_the_title()
+ )
+ );
+ $content = ob_get_clean();
+ }
+ if ( empty( $content ) ) {
+ return $excerpt;
+ } else {
+ return sprintf( '<div class="jetpack-blog-display jetpack-the-content">%s</div><div class="jetpack-blog-display jetpack-the-excerpt">%s</div>', $content, $excerpt );
+ }
+}
+
+/**
+ * Display Content instead of Excerpt in the Customizer when theme uses a 'Mixed' display.
+ */
+function jetpack_the_excerpt_mixed_customizer( $content ) {
+ if ( ( is_home() || is_archive() ) && ! is_post_type_archive( array( 'jetpack-testimonial', 'jetpack-portfolio', 'product' ) ) ) {
+ jetpack_the_content_customizer_class( 'output-the-excerpt' );
+ ob_start();
+ the_content();
+ $content = ob_get_clean();
+ }
+ return $content;
+}
+
+/**
+ * Returns a class value, `output-the-content` by default.
+ * Used for themes with a 'Mixed' Blog Display so we can tell which output is by default.
+ */
+function jetpack_the_content_customizer_class( $new_class = null ) {
+ static $class;
+ if ( isset( $new_class ) ) {
+ // Assign a new class and return.
+ $class = $new_class;
+ } elseif ( isset( $class ) ) {
+ // Reset the class after getting value.
+ $prev_class = $class;
+ $class = null;
+ return $prev_class;
+ } else {
+ // Return default class value.
+ return 'output-the-content';
+ }
+}
+
+if ( is_customize_preview() ) {
+ /*
+ * Display Content and Excerpt if the default Blog Display is 'Content'
+ * and we are in the Customizer.
+ */
+ if ( 'content' === $blog_display ) {
+ add_filter( 'the_content', 'jetpack_the_content_customizer' );
+ }
+
+ /*
+ * Display Content and Excerpt if the default Blog Display is 'Excerpt'
+ * and we are in the Customizer.
+ */
+ if ( 'excerpt' === $blog_display ) {
+ add_filter( 'the_excerpt', 'jetpack_the_excerpt_customizer' );
+ }
+
+ /*
+ * Display Content and Excerpt if the default Blog Display is 'Mixed'
+ * and we are in the Customizer.
+ */
+ if ( 'mixed' === $blog_display ) {
+ add_filter( 'the_content', 'jetpack_the_content_customizer' );
+ add_filter( 'the_excerpt', 'jetpack_the_excerpt_mixed_customizer' );
+ }
+} else {
+ $display_option = get_option( 'jetpack_content_blog_display', $blog_display );
+
+ /*
+ * Display Excerpt if the default Blog Display is 'Content'
+ * or default Blog Display is 'Mixed'
+ * and the Option picked is 'Post Excerpt'
+ * and we aren't in the Customizer.
+ */
+ if ( ( 'content' === $blog_display || 'mixed' === $blog_display ) && 'excerpt' === $display_option ) {
+ add_filter( 'the_content', 'jetpack_the_content_to_the_excerpt' );
+ }
+
+ /*
+ * Display Content if the default Blog Display is 'Excerpt'
+ * or default Blog Display is 'Mixed'
+ * and the Option picked is 'Full Post'
+ * and we aren't in the Customizer.
+ */
+ if ( ( 'excerpt' === $blog_display || 'mixed' === $blog_display ) && 'content' === $display_option ) {
+ add_filter( 'the_excerpt', 'jetpack_the_excerpt_to_the_content' );
+ }
+}
diff --git a/plugins/jetpack/modules/theme-tools/content-options/customizer.js b/plugins/jetpack/modules/theme-tools/content-options/customizer.js
new file mode 100644
index 00000000..afe7b492
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/content-options/customizer.js
@@ -0,0 +1,217 @@
+/* global blogDisplay, postDetails */
+
+/**
+ * customizer.js
+ *
+ * Theme Customizer enhancements for a better user experience.
+ *
+ * Contains handlers to make Theme Customizer preview reload changes asynchronously.
+ */
+
+( function( $ ) {
+ // Blog Display
+ wp.customize( 'jetpack_content_blog_display', function( value ) {
+ if ( 'content' === blogDisplay.display ) {
+ $( '.jetpack-blog-display.jetpack-the-excerpt' ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ position: 'absolute',
+ } );
+ $( '.jetpack-blog-display.jetpack-the-content' ).css( {
+ clip: 'auto',
+ position: 'relative',
+ } );
+ } else if ( 'excerpt' === blogDisplay.display ) {
+ $( '.jetpack-blog-display.jetpack-the-content' ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ position: 'absolute',
+ } );
+ $( '.jetpack-blog-display.jetpack-the-excerpt' ).css( {
+ clip: 'auto',
+ position: 'relative',
+ } );
+ } else if ( 'mixed' === blogDisplay.display ) {
+ $( '.jetpack-blog-display.jetpack-the-content.output-the-content' ).css( {
+ clip: 'auto',
+ position: 'relative',
+ } );
+ $( '.jetpack-blog-display.jetpack-the-excerpt.output-the-content' ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ position: 'absolute',
+ } );
+ $( '.jetpack-blog-display.jetpack-the-content.output-the-excerpt' ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ position: 'absolute',
+ } );
+ $( '.jetpack-blog-display.jetpack-the-excerpt.output-the-excerpt' ).css( {
+ clip: 'auto',
+ position: 'relative',
+ } );
+ }
+ value.bind( function( to ) {
+ if ( 'content' === to ) {
+ $( '.jetpack-blog-display.jetpack-the-excerpt' ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ position: 'absolute',
+ } );
+ $( '.jetpack-blog-display.jetpack-the-content' ).css( {
+ clip: 'auto',
+ position: 'relative',
+ } );
+ } else if ( 'excerpt' === to ) {
+ $( '.jetpack-blog-display.jetpack-the-content' ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ position: 'absolute',
+ } );
+ $( '.jetpack-blog-display.jetpack-the-excerpt' ).css( {
+ clip: 'auto',
+ position: 'relative',
+ } );
+ } else if ( 'mixed' === to ) {
+ $( '.jetpack-blog-display.jetpack-the-content.output-the-content' ).css( {
+ clip: 'auto',
+ position: 'relative',
+ } );
+ $( '.jetpack-blog-display.jetpack-the-excerpt.output-the-content' ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ position: 'absolute',
+ } );
+ $( '.jetpack-blog-display.jetpack-the-content.output-the-excerpt' ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ position: 'absolute',
+ } );
+ $( '.jetpack-blog-display.jetpack-the-excerpt.output-the-excerpt' ).css( {
+ clip: 'auto',
+ position: 'relative',
+ } );
+ }
+ if ( blogDisplay.masonry ) {
+ $( blogDisplay.masonry ).masonry();
+ }
+ } );
+ } );
+
+ // Post Details: Date.
+ wp.customize( 'jetpack_content_post_details_date', function( value ) {
+ value.bind( function( to ) {
+ if ( false === to ) {
+ $( postDetails.date ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ height: '1px',
+ overflow: 'hidden',
+ position: 'absolute',
+ width: '1px',
+ } );
+ $( 'body' ).addClass( 'date-hidden' );
+ } else {
+ $( postDetails.date ).css( {
+ clip: 'auto',
+ height: 'auto',
+ overflow: 'auto',
+ position: 'relative',
+ width: 'auto',
+ } );
+ $( 'body' ).removeClass( 'date-hidden' );
+ }
+ } );
+ } );
+
+ // Post Details: Categories.
+ wp.customize( 'jetpack_content_post_details_categories', function( value ) {
+ value.bind( function( to ) {
+ if ( false === to ) {
+ $( postDetails.categories ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ height: '1px',
+ overflow: 'hidden',
+ position: 'absolute',
+ width: '1px',
+ } );
+ $( 'body' ).addClass( 'categories-hidden' );
+ } else {
+ $( postDetails.categories ).css( {
+ clip: 'auto',
+ height: 'auto',
+ overflow: 'auto',
+ position: 'relative',
+ width: 'auto',
+ } );
+ $( 'body' ).removeClass( 'categories-hidden' );
+ }
+ } );
+ } );
+
+ // Post Details: Tags.
+ wp.customize( 'jetpack_content_post_details_tags', function( value ) {
+ value.bind( function( to ) {
+ if ( false === to ) {
+ $( postDetails.tags ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ height: '1px',
+ overflow: 'hidden',
+ position: 'absolute',
+ width: '1px',
+ } );
+ $( 'body' ).addClass( 'tags-hidden' );
+ } else {
+ $( postDetails.tags ).css( {
+ clip: 'auto',
+ height: 'auto',
+ overflow: 'auto',
+ position: 'relative',
+ width: 'auto',
+ } );
+ $( 'body' ).removeClass( 'tags-hidden' );
+ }
+ } );
+ } );
+
+ // Post Details: Author.
+ wp.customize( 'jetpack_content_post_details_author', function( value ) {
+ value.bind( function( to ) {
+ if ( false === to ) {
+ $( postDetails.author ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ height: '1px',
+ overflow: 'hidden',
+ position: 'absolute',
+ width: '1px',
+ } );
+ $( 'body' ).addClass( 'author-hidden' );
+ } else {
+ $( postDetails.author ).css( {
+ clip: 'auto',
+ height: 'auto',
+ overflow: 'auto',
+ position: 'relative',
+ width: 'auto',
+ } );
+ $( 'body' ).removeClass( 'author-hidden' );
+ }
+ } );
+ } );
+
+ // Post Details: Comment link.
+ wp.customize( 'jetpack_content_post_details_comment', function( value ) {
+ value.bind( function( to ) {
+ if ( false === to ) {
+ $( postDetails.comment ).css( {
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ height: '1px',
+ overflow: 'hidden',
+ position: 'absolute',
+ width: '1px',
+ } );
+ $( 'body' ).addClass( 'comment-hidden' );
+ } else {
+ $( postDetails.comment ).css( {
+ clip: 'auto',
+ height: 'auto',
+ overflow: 'auto',
+ position: 'relative',
+ width: 'auto',
+ } );
+ $( 'body' ).removeClass( 'comment-hidden' );
+ }
+ } );
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/theme-tools/content-options/customizer.php b/plugins/jetpack/modules/theme-tools/content-options/customizer.php
new file mode 100644
index 00000000..844e5074
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/content-options/customizer.php
@@ -0,0 +1,482 @@
+<?php
+/**
+ * Add Content section to the Theme Customizer.
+ *
+ * @param WP_Customize_Manager $wp_customize Theme Customizer object.
+ */
+function jetpack_content_options_customize_register( $wp_customize ) {
+ $options = get_theme_support( 'jetpack-content-options' );
+ $blog_display = ( ! empty( $options[0]['blog-display'] ) ) ? $options[0]['blog-display'] : null;
+ $blog_display = preg_grep( '/^(content|excerpt)$/', (array) $blog_display );
+ sort( $blog_display );
+ $blog_display = implode( ', ', $blog_display );
+ $blog_display = ( 'content, excerpt' === $blog_display ) ? 'mixed' : $blog_display;
+ $author_bio = ( ! empty( $options[0]['author-bio'] ) ) ? $options[0]['author-bio'] : null;
+ $author_bio_default = ( isset( $options[0]['author-bio-default'] ) && false === $options[0]['author-bio-default'] ) ? '' : 1;
+ $post_details = ( ! empty( $options[0]['post-details'] ) ) ? $options[0]['post-details'] : null;
+ $date = ( ! empty( $post_details['date'] ) ) ? $post_details['date'] : null;
+ $categories = ( ! empty( $post_details['categories'] ) ) ? $post_details['categories'] : null;
+ $tags = ( ! empty( $post_details['tags'] ) ) ? $post_details['tags'] : null;
+ $author = ( ! empty( $post_details['author'] ) ) ? $post_details['author'] : null;
+ $comment = ( ! empty( $post_details['comment'] ) ) ? $post_details['comment'] : null;
+ $featured_images = ( ! empty( $options[0]['featured-images'] ) ) ? $options[0]['featured-images'] : null;
+ $fi_archive = ( ! empty( $featured_images['archive'] ) ) ? $featured_images['archive'] : null;
+ $fi_post = ( ! empty( $featured_images['post'] ) ) ? $featured_images['post'] : null;
+ $fi_page = ( ! empty( $featured_images['page'] ) ) ? $featured_images['page'] : null;
+ $fi_portfolio = ( ! empty( $featured_images['portfolio'] ) ) ? $featured_images['portfolio'] : null;
+ $fi_fallback = ( ! empty( $featured_images['fallback'] ) ) ? $featured_images['fallback'] : null;
+ $fi_archive_default = ( isset( $featured_images['archive-default'] ) && false === $featured_images['archive-default'] ) ? '' : 1;
+ $fi_post_default = ( isset( $featured_images['post-default'] ) && false === $featured_images['post-default'] ) ? '' : 1;
+ $fi_page_default = ( isset( $featured_images['page-default'] ) && false === $featured_images['page-default'] ) ? '' : 1;
+ $fi_portfolio_default = ( isset( $featured_images['portfolio-default'] ) && false === $featured_images['portfolio-default'] ) ? '' : 1;
+ $fi_fallback_default = ( isset( $featured_images['fallback-default'] ) && false === $featured_images['fallback-default'] ) ? '' : 1;
+
+ // If the theme doesn't support 'jetpack-content-options[ 'blog-display' ]', 'jetpack-content-options[ 'author-bio' ]', 'jetpack-content-options[ 'post-details' ]' and 'jetpack-content-options[ 'featured-images' ]', don't continue.
+ if ( ( ! in_array( $blog_display, array( 'content', 'excerpt', 'mixed' ) ) )
+ && ( true !== $author_bio )
+ && ( ( empty( $post_details['stylesheet'] ) )
+ && ( empty( $date )
+ || empty( $categories )
+ || empty( $tags )
+ || empty( $author )
+ || empty( $comment ) ) )
+ && ( true !== $fi_archive && true !== $fi_post && true !== $fi_page && true !== $fi_portfolio && true !== $fi_fallback ) ) {
+ return;
+ }
+
+ // New control type: Title.
+ class Jetpack_Customize_Control_Title extends WP_Customize_Control {
+ public $type = 'title';
+
+ public function render_content() {
+ ?>
+ <span class="customize-control-title"><?php echo wp_kses_post( $this->label ); ?></span>
+ <?php
+ }
+ }
+
+ // Add Content section.
+ $wp_customize->add_section(
+ 'jetpack_content_options',
+ array(
+ 'title' => esc_html__( 'Content Options', 'jetpack' ),
+ 'theme_supports' => 'jetpack-content-options',
+ 'priority' => 100,
+ )
+ );
+
+ // Add Blog Display option.
+ if ( in_array( $blog_display, array( 'content', 'excerpt', 'mixed' ) ) ) {
+ if ( 'mixed' === $blog_display ) {
+ $blog_display_choices = array(
+ 'content' => esc_html__( 'Full post', 'jetpack' ),
+ 'excerpt' => esc_html__( 'Post excerpt', 'jetpack' ),
+ 'mixed' => esc_html__( 'Default', 'jetpack' ),
+ );
+
+ $blog_display_description = esc_html__( 'Choose between a full post or an excerpt for the blog and archive pages, or opt for the theme\'s default combination of excerpt and full post.', 'jetpack' );
+ } else {
+ $blog_display_choices = array(
+ 'content' => esc_html__( 'Full post', 'jetpack' ),
+ 'excerpt' => esc_html__( 'Post excerpt', 'jetpack' ),
+ );
+
+ $blog_display_description = esc_html__( 'Choose between a full post or an excerpt for the blog and archive pages.', 'jetpack' );
+
+ if ( 'mixed' === get_option( 'jetpack_content_blog_display' ) ) {
+ update_option( 'jetpack_content_blog_display', $blog_display );
+ }
+ }
+
+ $wp_customize->add_setting(
+ 'jetpack_content_blog_display',
+ array(
+ 'default' => $blog_display,
+ 'type' => 'option',
+ 'transport' => 'postMessage',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_blog_display',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_blog_display',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Blog Display', 'jetpack' ),
+ 'description' => $blog_display_description,
+ 'type' => 'radio',
+ 'choices' => $blog_display_choices,
+ )
+ );
+ }
+
+ // Add Author Bio option.
+ if ( true === $author_bio ) {
+ $wp_customize->add_setting( 'jetpack_content_author_bio_title' );
+
+ $wp_customize->add_control(
+ new Jetpack_Customize_Control_Title(
+ $wp_customize,
+ 'jetpack_content_author_bio_title',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Author Bio', 'jetpack' ),
+ 'type' => 'title',
+ )
+ )
+ );
+
+ $wp_customize->add_setting(
+ 'jetpack_content_author_bio',
+ array(
+ 'default' => $author_bio_default,
+ 'type' => 'option',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_author_bio',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Display on single posts', 'jetpack' ),
+ 'type' => 'checkbox',
+ )
+ );
+ }
+
+ // Add Post Details options.
+ if ( ( ! empty( $post_details ) )
+ && ( ! empty( $post_details['stylesheet'] ) )
+ && ( ! empty( $date )
+ || ! empty( $categories )
+ || ! empty( $tags )
+ || ! empty( $author )
+ || ! empty( $comment ) ) ) {
+ $wp_customize->add_setting( 'jetpack_content_post_details_title' );
+
+ $wp_customize->add_control(
+ new Jetpack_Customize_Control_Title(
+ $wp_customize,
+ 'jetpack_content_post_details_title',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Post Details', 'jetpack' ),
+ 'type' => 'title',
+ )
+ )
+ );
+
+ // Post Details: Date
+ if ( ! empty( $date ) ) {
+ $wp_customize->add_setting(
+ 'jetpack_content_post_details_date',
+ array(
+ 'default' => 1,
+ 'type' => 'option',
+ 'transport' => 'postMessage',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_post_details_date',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Display date', 'jetpack' ),
+ 'type' => 'checkbox',
+ )
+ );
+ }
+
+ // Post Details: Categories
+ if ( ! empty( $categories ) ) {
+ $wp_customize->add_setting(
+ 'jetpack_content_post_details_categories',
+ array(
+ 'default' => 1,
+ 'type' => 'option',
+ 'transport' => 'postMessage',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_post_details_categories',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Display categories', 'jetpack' ),
+ 'type' => 'checkbox',
+ )
+ );
+ }
+
+ // Post Details: Tags
+ if ( ! empty( $tags ) ) {
+ $wp_customize->add_setting(
+ 'jetpack_content_post_details_tags',
+ array(
+ 'default' => 1,
+ 'type' => 'option',
+ 'transport' => 'postMessage',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_post_details_tags',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Display tags', 'jetpack' ),
+ 'type' => 'checkbox',
+ )
+ );
+ }
+
+ // Post Details: Author
+ if ( ! empty( $author ) ) {
+ $wp_customize->add_setting(
+ 'jetpack_content_post_details_author',
+ array(
+ 'default' => 1,
+ 'type' => 'option',
+ 'transport' => 'postMessage',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_post_details_author',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Display author', 'jetpack' ),
+ 'type' => 'checkbox',
+ )
+ );
+ }
+
+ // Post Details: Comment link
+ if ( ! empty( $comment ) ) {
+ $wp_customize->add_setting(
+ 'jetpack_content_post_details_comment',
+ array(
+ 'default' => 1,
+ 'type' => 'option',
+ 'transport' => 'postMessage',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_post_details_comment',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Display comment link', 'jetpack' ),
+ 'type' => 'checkbox',
+ )
+ );
+ }
+ }
+
+ // Add Featured Images options.
+ if ( true === $fi_archive || true === $fi_post || true === $fi_page || true === $fi_portfolio || true === $fi_fallback ) {
+ $wp_customize->add_setting( 'jetpack_content_featured_images_title' );
+
+ $wp_customize->add_control(
+ new Jetpack_Customize_Control_Title(
+ $wp_customize,
+ 'jetpack_content_featured_images_title',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Featured Images', 'jetpack' ) . sprintf( '<a href="https://en.support.wordpress.com/featured-images/" class="customize-help-toggle dashicons dashicons-editor-help" title="%1$s" rel="noopener noreferrer" target="_blank"><span class="screen-reader-text">%1$s</span></a>', esc_html__( 'Learn more about Featured Images', 'jetpack' ) ),
+ 'type' => 'title',
+ 'active_callback' => 'jetpack_post_thumbnail_supports',
+ )
+ )
+ );
+
+ // Featured Images: Archive
+ if ( true === $fi_archive ) {
+ $wp_customize->add_setting(
+ 'jetpack_content_featured_images_archive',
+ array(
+ 'default' => $fi_archive_default,
+ 'type' => 'option',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_featured_images_archive',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Display on blog and archives', 'jetpack' ),
+ 'type' => 'checkbox',
+ 'active_callback' => 'jetpack_post_thumbnail_supports',
+ )
+ );
+ }
+
+ // Featured Images: Post
+ if ( true === $fi_post ) {
+ $wp_customize->add_setting(
+ 'jetpack_content_featured_images_post',
+ array(
+ 'default' => $fi_post_default,
+ 'type' => 'option',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_featured_images_post',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Display on single posts', 'jetpack' ),
+ 'type' => 'checkbox',
+ 'active_callback' => 'jetpack_post_thumbnail_supports',
+ )
+ );
+ }
+
+ // Featured Images: Page
+ if ( true === $fi_page ) {
+ $wp_customize->add_setting(
+ 'jetpack_content_featured_images_page',
+ array(
+ 'default' => $fi_page_default,
+ 'type' => 'option',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_featured_images_page',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Display on pages', 'jetpack' ),
+ 'type' => 'checkbox',
+ 'active_callback' => 'jetpack_post_thumbnail_supports',
+ )
+ );
+ }
+
+ // Featured Images: Portfolio
+ if ( true === $fi_portfolio && post_type_exists( 'jetpack-portfolio' ) ) {
+ $wp_customize->add_setting(
+ 'jetpack_content_featured_images_portfolio',
+ array(
+ 'default' => $fi_portfolio_default,
+ 'type' => 'option',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_featured_images_portfolio',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Display on single projects', 'jetpack' ),
+ 'type' => 'checkbox',
+ 'active_callback' => 'jetpack_post_thumbnail_supports',
+ )
+ );
+ }
+
+ // Featured Images: Fallback
+ if ( true === $fi_fallback ) {
+ $wp_customize->add_setting(
+ 'jetpack_content_featured_images_fallback',
+ array(
+ 'default' => $fi_fallback_default,
+ 'type' => 'option',
+ 'sanitize_callback' => 'jetpack_content_options_sanitize_checkbox',
+ )
+ );
+
+ $wp_customize->add_control(
+ 'jetpack_content_featured_images_fallback',
+ array(
+ 'section' => 'jetpack_content_options',
+ 'label' => esc_html__( 'Automatically use first image in post', 'jetpack' ),
+ 'type' => 'checkbox',
+ 'active_callback' => 'jetpack_post_thumbnail_supports',
+ )
+ );
+ }
+ }
+}
+add_action( 'customize_register', 'jetpack_content_options_customize_register' );
+
+/**
+ * Return whether the theme supports Post Thumbnails.
+ */
+function jetpack_post_thumbnail_supports() {
+ return ( current_theme_supports( 'post-thumbnails' ) );
+}
+
+/**
+ * Sanitize the checkbox.
+ *
+ * @param int $input.
+ * @return boolean|string
+ */
+function jetpack_content_options_sanitize_checkbox( $input ) {
+ return ( 1 == $input ) ? 1 : '';
+}
+
+/**
+ * Sanitize the Display value.
+ *
+ * @param string $display.
+ * @return string.
+ */
+function jetpack_content_options_sanitize_blog_display( $display ) {
+ if ( ! in_array( $display, array( 'content', 'excerpt', 'mixed' ) ) ) {
+ $display = 'content';
+ }
+ return $display;
+}
+
+/**
+ * Binds JS handlers to make Theme Customizer preview reload changes asynchronously.
+ */
+function jetpack_content_options_customize_preview_js() {
+ $options = get_theme_support( 'jetpack-content-options' );
+ $blog_display = ( ! empty( $options[0]['blog-display'] ) ) ? $options[0]['blog-display'] : null;
+ $blog_display = preg_grep( '/^(content|excerpt)$/', (array) $blog_display );
+ sort( $blog_display );
+ $blog_display = implode( ', ', $blog_display );
+ $blog_display = ( 'content, excerpt' === $blog_display ) ? 'mixed' : $blog_display;
+ $masonry = ( ! empty( $options[0]['masonry'] ) ) ? $options[0]['masonry'] : null;
+ $post_details = ( ! empty( $options[0]['post-details'] ) ) ? $options[0]['post-details'] : null;
+ $date = ( ! empty( $post_details['date'] ) ) ? $post_details['date'] : null;
+ $categories = ( ! empty( $post_details['categories'] ) ) ? $post_details['categories'] : null;
+ $tags = ( ! empty( $post_details['tags'] ) ) ? $post_details['tags'] : null;
+ $author = ( ! empty( $post_details['author'] ) ) ? $post_details['author'] : null;
+ $comment = ( ! empty( $post_details['comment'] ) ) ? $post_details['comment'] : null;
+
+ wp_enqueue_script( 'jetpack-content-options-customizer', plugins_url( 'customizer.js', __FILE__ ), array( 'customize-preview' ), '1.0', true );
+
+ wp_localize_script(
+ 'jetpack-content-options-customizer',
+ 'blogDisplay',
+ array(
+ 'display' => get_option( 'jetpack_content_blog_display', $blog_display ),
+ 'masonry' => $masonry,
+ )
+ );
+
+ wp_localize_script(
+ 'jetpack-content-options-customizer',
+ 'postDetails',
+ array(
+ 'date' => $date,
+ 'categories' => $categories,
+ 'tags' => $tags,
+ 'author' => $author,
+ 'comment' => $comment,
+ )
+ );
+}
+add_action( 'customize_preview_init', 'jetpack_content_options_customize_preview_js' );
diff --git a/plugins/jetpack/modules/theme-tools/content-options/featured-images-fallback.php b/plugins/jetpack/modules/theme-tools/content-options/featured-images-fallback.php
new file mode 100644
index 00000000..dc8d92d6
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/content-options/featured-images-fallback.php
@@ -0,0 +1,161 @@
+<?php
+/**
+ * Get one image from a specified post in the following order:
+ * Featured Image then first image from the_content HTML
+ * and filter the post_thumbnail_html
+ *
+ * @param string $html The HTML for the image markup.
+ * @param int $post_id The post ID to check.
+ * @param int $post_thumbnail_id The ID of the featured image.
+ * @param string $size The image size to return, defaults to 'post-thumbnail'.
+ * @param string|array $attr Optional. Query string or array of attributes.
+ *
+ * @return string $html Thumbnail image with markup.
+ */
+function jetpack_featured_images_fallback_get_image( $html, $post_id, $post_thumbnail_id, $size, $attr ) {
+ $opts = jetpack_featured_images_get_settings();
+
+ if ( ! empty( $html ) || (bool) 1 !== (bool) $opts['fallback-option'] ) {
+ return trim( $html );
+ }
+
+ if ( jetpack_featured_images_should_load() ) {
+ if (
+ ( true === $opts['archive'] && ( is_home() || is_archive() || is_search() ) && ! $opts['archive-option'] )
+ || ( true === $opts['post'] && is_single() && ! $opts['post-option'] )
+ || ! $opts['fallback-option']
+ ) {
+ return trim( $html );
+ }
+ }
+
+ if ( class_exists( 'Jetpack_PostImages' ) ) {
+ global $_wp_additional_image_sizes;
+
+ $args = array(
+ 'from_thumbnail' => false,
+ 'from_slideshow' => true,
+ 'from_gallery' => true,
+ 'from_attachment' => false,
+ );
+
+ $image = Jetpack_PostImages::get_image( $post_id, $args );
+
+ if ( ! empty( $image ) ) {
+ $image['width'] = '';
+ $image['height'] = '';
+ $image['crop'] = '';
+
+ if ( array_key_exists( $size, $_wp_additional_image_sizes ) ) {
+ $image['width'] = $_wp_additional_image_sizes[ $size ]['width'];
+ $image['height'] = $_wp_additional_image_sizes[ $size ]['height'];
+ $image['crop'] = $_wp_additional_image_sizes[ $size ]['crop'];
+ }
+
+ $image_src = Jetpack_PostImages::fit_image_url( $image['src'], $image['width'], $image['height'] );
+
+ // Use the theme's crop setting rather than forcing to true
+ $image_src = add_query_arg( 'crop', $image['crop'], $image_src );
+
+ $html = '<img src="' . esc_url( $image_src ) . '" title="' . esc_attr( strip_tags( get_the_title() ) ) . '" class="attachment-' . esc_attr( $size ) . ' wp-post-image" />';
+
+ return trim( $html );
+ }
+ }
+
+ return trim( $html );
+}
+add_filter( 'post_thumbnail_html', 'jetpack_featured_images_fallback_get_image', 10, 5 );
+
+/**
+ * Get URL of one image from a specified post in the following order:
+ * Featured Image then first image from the_content HTML
+ *
+ * @param int $post_id The post ID to check.
+ * @param int $post_thumbnail_id The ID of the featured image.
+ * @param string $size The image size to return, defaults to 'post-thumbnail'.
+ *
+ * @return string|null $image_src The URL of the thumbnail image.
+ */
+function jetpack_featured_images_fallback_get_image_src( $post_id, $post_thumbnail_id, $size ) {
+ $image_src = wp_get_attachment_image_src( $post_thumbnail_id, $size );
+ $image_src = ( ! empty( $image_src[0] ) ) ? $image_src[0] : null;
+ $opts = jetpack_featured_images_get_settings();
+
+ if ( ! empty( $image_src ) || (bool) 1 !== (bool) $opts['fallback-option'] ) {
+ return esc_url( $image_src );
+ }
+
+ if ( jetpack_featured_images_should_load() ) {
+ if ( ( true === $opts['archive'] && ( is_home() || is_archive() || is_search() ) && ! $opts['archive-option'] )
+ || ( true === $opts['post'] && is_single() && ! $opts['post-option'] ) ) {
+ return esc_url( $image_src );
+ }
+ }
+
+ if ( class_exists( 'Jetpack_PostImages' ) ) {
+ global $_wp_additional_image_sizes;
+
+ $args = array(
+ 'from_thumbnail' => false,
+ 'from_slideshow' => true,
+ 'from_gallery' => true,
+ 'from_attachment' => false,
+ );
+
+ $image = Jetpack_PostImages::get_image( $post_id, $args );
+
+ if ( ! empty( $image ) ) {
+ $image['width'] = '';
+ $image['height'] = '';
+ $image['crop'] = '';
+
+ if ( array_key_exists( $size, $_wp_additional_image_sizes ) ) {
+ $image['width'] = $_wp_additional_image_sizes[ $size ]['width'];
+ $image['height'] = $_wp_additional_image_sizes[ $size ]['height'];
+ $image['crop'] = $_wp_additional_image_sizes[ $size ]['crop'];
+ }
+
+ $image_src = Jetpack_PostImages::fit_image_url( $image['src'], $image['width'], $image['height'] );
+
+ // Use the theme's crop setting rather than forcing to true
+ $image_src = add_query_arg( 'crop', $image['crop'], $image_src );
+
+ return esc_url( $image_src );
+ }
+ }
+
+ return esc_url( $image_src );
+}
+
+/**
+ * Check if post has an image attached, including a fallback.
+ *
+ * @param int $post The post ID to check.
+ *
+ * @return bool
+ */
+function jetpack_has_featured_image( $post = null ) {
+ return (bool) get_the_post_thumbnail( $post );
+}
+
+/**
+ * Adds custom class to the array of post classes.
+ *
+ * @param array $classes Classes for the post element.
+ * @param array $class Optional. Comma separated list of additional classes.
+ * @param array $post_id Unique The post ID to check
+ *
+ * @return array $classes
+ */
+function jetpack_featured_images_post_class( $classes, $class, $post_id ) {
+ $post_password_required = post_password_required( $post_id );
+ $opts = jetpack_featured_images_get_settings();
+
+ if ( jetpack_has_featured_image( $post_id ) && (bool) 1 === (bool) $opts['fallback-option'] && ! is_attachment() && ! $post_password_required && 'post' === get_post_type() ) {
+ $classes[] = 'has-post-thumbnail';
+ }
+
+ return $classes;
+}
+add_filter( 'post_class', 'jetpack_featured_images_post_class', 10, 3 );
diff --git a/plugins/jetpack/modules/theme-tools/content-options/featured-images.php b/plugins/jetpack/modules/theme-tools/content-options/featured-images.php
new file mode 100644
index 00000000..d6855f6c
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/content-options/featured-images.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * The function to prevent for Featured Images to be displayed in a theme.
+ */
+function jetpack_featured_images_remove_post_thumbnail( $metadata, $object_id, $meta_key, $single ) {
+ $opts = jetpack_featured_images_get_settings();
+
+ // Automatically return metadata if it's a PayPal product - we don't want to hide the Featured Image.
+ if ( 'jp_pay_product' === get_post_type( $object_id ) ) {
+ return $metadata;
+ }
+
+ // Return false if the archive option or singular option is unticked.
+ if (
+ ( true === $opts['archive']
+ && ( is_home() || is_archive() || is_search() )
+ && ! jetpack_is_shop_page()
+ && ! $opts['archive-option']
+ && ( isset( $meta_key )
+ && '_thumbnail_id' === $meta_key )
+ && in_the_loop()
+ )
+ || ( true === $opts['post']
+ && is_single()
+ && ! jetpack_is_product()
+ && ! $opts['post-option']
+ && ( isset( $meta_key )
+ && '_thumbnail_id' === $meta_key )
+ && in_the_loop()
+ )
+ || ( true === $opts['page']
+ && is_singular()
+ && is_page()
+ && ! $opts['page-option']
+ && ( isset( $meta_key )
+ && '_thumbnail_id' === $meta_key )
+ && in_the_loop()
+ )
+ || ( true === $opts['portfolio']
+ && post_type_exists( 'jetpack-portfolio' )
+ && is_singular( 'jetpack-portfolio' )
+ && ! $opts['portfolio-option']
+ && ( isset( $meta_key )
+ && '_thumbnail_id' === $meta_key )
+ && in_the_loop()
+ )
+ ) {
+ return false;
+ } else {
+ return $metadata;
+ }
+}
+add_filter( 'get_post_metadata', 'jetpack_featured_images_remove_post_thumbnail', true, 4 );
+
+/**
+ * Check if we are in a WooCommerce Product in order to exclude it from the is_single check.
+ */
+function jetpack_is_product() {
+ return ( function_exists( 'is_product' ) ) ? is_product() : false;
+}
+
+/**
+ * Check if we are in a WooCommerce Shop in order to exclude it from the is_archive check.
+ */
+function jetpack_is_shop_page() {
+ // Check if WooCommerce is active first.
+ if ( ! class_exists( 'WooCommerce' ) ) {
+ return false;
+ }
+
+ global $wp_query;
+
+ $front_page_id = get_option( 'page_on_front' );
+ $current_page_id = $wp_query->get( 'page_id' );
+ $is_static_front_page = 'page' === get_option( 'show_on_front' );
+
+ if ( $is_static_front_page && $front_page_id === $current_page_id ) {
+ $is_shop_page = ( $current_page_id === wc_get_page_id( 'shop' ) ) ? true : false;
+ } else {
+ $is_shop_page = is_shop();
+ }
+
+ return $is_shop_page;
+}
diff --git a/plugins/jetpack/modules/theme-tools/content-options/post-details.php b/plugins/jetpack/modules/theme-tools/content-options/post-details.php
new file mode 100644
index 00000000..7851a5be
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/content-options/post-details.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * The function to include Post Details in a theme's stylesheet.
+ */
+function jetpack_post_details_enqueue_scripts() {
+ // Make sure we can proceed.
+ list( $should_run, $options, $definied, $post_details ) = jetpack_post_details_should_run();
+
+ if ( ! $should_run ) {
+ return;
+ }
+
+ list( $date_option, $categories_option, $tags_option, $author_option, $comment_option ) = $options;
+ list( $date, $categories, $tags, $author, $comment ) = $definied;
+
+ $elements = array();
+
+ // If date option is unticked, add it to the list of classes.
+ if ( 1 != $date_option && ! empty( $date ) ) {
+ $elements[] = $date;
+ }
+
+ // If categories option is unticked, add it to the list of classes.
+ if ( 1 != $categories_option && ! empty( $categories ) ) {
+ $elements[] = $categories;
+ }
+
+ // If tags option is unticked, add it to the list of classes.
+ if ( 1 != $tags_option && ! empty( $tags ) ) {
+ $elements[] = $tags;
+ }
+
+ // If author option is unticked, add it to the list of classes.
+ if ( 1 != $author_option && ! empty( $author ) ) {
+ $elements[] = $author;
+ }
+
+ // If comment option is unticked, add it to the list of classes.
+ if ( 1 != $comment_option && ! empty( $comment ) ) {
+ $elements[] = $comment;
+ }
+
+ // Get the list of classes.
+ $elements = implode( ', ', $elements );
+
+ // Hide the classes with CSS.
+ $css = $elements . ' { clip: rect(1px, 1px, 1px, 1px); height: 1px; position: absolute; overflow: hidden; width: 1px; }';
+
+ // Add the CSS to the stylesheet.
+ wp_add_inline_style( $post_details['stylesheet'], $css );
+}
+add_action( 'wp_enqueue_scripts', 'jetpack_post_details_enqueue_scripts' );
+
+/**
+ * Adds custom classes to the array of body classes.
+ */
+function jetpack_post_details_body_classes( $classes ) {
+ // Make sure we can proceed.
+ list( $should_run, $options, $definied ) = jetpack_post_details_should_run();
+
+ if ( ! $should_run ) {
+ return $classes;
+ }
+
+ list( $date_option, $categories_option, $tags_option, $author_option, $comment_option ) = $options;
+ list( $date, $categories, $tags, $author, $comment ) = $definied;
+
+ // If date option is unticked, add a class of 'date-hidden' to the body.
+ if ( 1 != $date_option && ! empty( $date ) ) {
+ $classes[] = 'date-hidden';
+ }
+
+ // If categories option is unticked, add a class of 'categories-hidden' to the body.
+ if ( 1 != $categories_option && ! empty( $categories ) ) {
+ $classes[] = 'categories-hidden';
+ }
+
+ // If tags option is unticked, add a class of 'tags-hidden' to the body.
+ if ( 1 != $tags_option && ! empty( $tags ) ) {
+ $classes[] = 'tags-hidden';
+ }
+
+ // If author option is unticked, add a class of 'author-hidden' to the body.
+ if ( 1 != $author_option && ! empty( $author ) ) {
+ $classes[] = 'author-hidden';
+ }
+
+ // If comment option is unticked, add a class of 'comment-hidden' to the body.
+ if ( 1 != $comment_option && ! empty( $comment ) ) {
+ $classes[] = 'comment-hidden';
+ }
+
+ return $classes;
+}
+add_filter( 'body_class', 'jetpack_post_details_body_classes' );
+
+/**
+ * Determines if Post Details should run.
+ */
+function jetpack_post_details_should_run() {
+ // Empty value representing falsy return value.
+ $void = array( false, null, null, null );
+
+ // If the theme doesn't support 'jetpack-content-options', don't continue.
+ if ( ! current_theme_supports( 'jetpack-content-options' ) ) {
+ return $void;
+ }
+
+ $options = get_theme_support( 'jetpack-content-options' );
+ $post_details = ( ! empty( $options[0]['post-details'] ) ) ? $options[0]['post-details'] : null;
+
+ // If the theme doesn't support 'jetpack-content-options[ 'post-details' ]', don't continue.
+ if ( empty( $post_details ) ) {
+ return $void;
+ }
+
+ $date = ( ! empty( $post_details['date'] ) ) ? $post_details['date'] : null;
+ $categories = ( ! empty( $post_details['categories'] ) ) ? $post_details['categories'] : null;
+ $tags = ( ! empty( $post_details['tags'] ) ) ? $post_details['tags'] : null;
+ $author = ( ! empty( $post_details['author'] ) ) ? $post_details['author'] : null;
+ $comment = ( ! empty( $post_details['comment'] ) ) ? $post_details['comment'] : null;
+
+ // If there is no stylesheet and there are no date, categories, tags, author or comment declared, don't continue.
+ if (
+ empty( $post_details['stylesheet'] )
+ && ( empty( $date )
+ || empty( $categories )
+ || empty( $tags )
+ || empty( $author )
+ || empty( $comment ) )
+ ) {
+ return $void;
+ }
+
+ $date_option = get_option( 'jetpack_content_post_details_date', 1 );
+ $categories_option = get_option( 'jetpack_content_post_details_categories', 1 );
+ $tags_option = get_option( 'jetpack_content_post_details_tags', 1 );
+ $author_option = get_option( 'jetpack_content_post_details_author', 1 );
+ $comment_option = get_option( 'jetpack_content_post_details_comment', 1 );
+
+ $options = array( $date_option, $categories_option, $tags_option, $author_option, $comment_option );
+ $definied = array( $date, $categories, $tags, $author, $comment );
+
+ // If all the options are ticked, don't continue.
+ if ( array( 1, 1, 1, 1, 1 ) === $options ) {
+ return $void;
+ }
+
+ return array( true, $options, $definied, $post_details );
+}
diff --git a/plugins/jetpack/modules/theme-tools/featured-content.php b/plugins/jetpack/modules/theme-tools/featured-content.php
new file mode 100644
index 00000000..1df0f069
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/featured-content.php
@@ -0,0 +1,722 @@
+<?php
+
+if ( ! class_exists( 'Featured_Content' ) && isset( $GLOBALS['pagenow'] ) && 'plugins.php' !== $GLOBALS['pagenow'] ) {
+
+ /**
+ * Featured Content.
+ *
+ * This module will allow users to define a subset of posts to be displayed in a
+ * theme-designated featured content area.
+ *
+ * This feature will only be activated for themes that declare that they support
+ * it. This can be done by adding code similar to the following during the
+ * "after_setup_theme" action:
+ *
+ * add_theme_support( 'featured-content', array(
+ * 'filter' => 'mytheme_get_featured_content',
+ * 'max_posts' => 20,
+ * 'post_types' => array( 'post', 'page' ),
+ * ) );
+ *
+ * For maximum compatibility with different methods of posting users will
+ * designate a featured post tag to associate posts with. Since this tag now has
+ * special meaning beyond that of a normal tags, users will have the ability to
+ * hide it from the front-end of their site.
+ */
+ class Featured_Content {
+
+ /**
+ * The maximum number of posts that a Featured Content area can contain. We
+ * define a default value here but themes can override this by defining a
+ * "max_posts" entry in the second parameter passed in the call to
+ * add_theme_support( 'featured-content' ).
+ *
+ * @see Featured_Content::init()
+ */
+ public static $max_posts = 15;
+
+ /**
+ * The registered post types supported by Featured Content. Themes can add
+ * Featured Content support for registered post types by defining a
+ * 'post_types' argument (string|array) in the call to
+ * add_theme_support( 'featured-content' ).
+ *
+ * @see Featured_Content::init()
+ */
+ public static $post_types = array( 'post' );
+
+ /**
+ * The tag that is used to mark featured content. Users can define
+ * a custom tag name that will be stored in this variable.
+ *
+ * @see Featured_Content::hide_featured_term
+ */
+ public static $tag;
+
+ /**
+ * Instantiate.
+ *
+ * All custom functionality will be hooked into the "init" action.
+ */
+ public static function setup() {
+ add_action( 'init', array( __CLASS__, 'init' ), 30 );
+ }
+
+ /**
+ * Conditionally hook into WordPress.
+ *
+ * Themes must declare that they support this module by adding
+ * add_theme_support( 'featured-content' ); during after_setup_theme.
+ *
+ * If no theme support is found there is no need to hook into WordPress. We'll
+ * just return early instead.
+ *
+ * @uses Featured_Content::$max_posts
+ */
+ public static function init() {
+ $theme_support = get_theme_support( 'featured-content' );
+
+ // Return early if theme does not support featured content.
+ if ( ! $theme_support ) {
+ return;
+ }
+
+ /*
+ * An array of named arguments must be passed as the second parameter
+ * of add_theme_support().
+ */
+ if ( ! isset( $theme_support[0] ) ) {
+ return;
+ }
+
+ if ( isset( $theme_support[0]['featured_content_filter'] ) ) {
+ $theme_support[0]['filter'] = $theme_support[0]['featured_content_filter'];
+ unset( $theme_support[0]['featured_content_filter'] );
+ }
+
+ // Return early if "filter" has not been defined.
+ if ( ! isset( $theme_support[0]['filter'] ) ) {
+ return;
+ }
+
+ // Theme can override the number of max posts.
+ if ( isset( $theme_support[0]['max_posts'] ) ) {
+ self::$max_posts = absint( $theme_support[0]['max_posts'] );
+ }
+
+ add_filter( $theme_support[0]['filter'], array( __CLASS__, 'get_featured_posts' ) );
+ add_action( 'customize_register', array( __CLASS__, 'customize_register' ), 9 );
+ add_action( 'admin_init', array( __CLASS__, 'register_setting' ) );
+ add_action( 'save_post', array( __CLASS__, 'delete_transient' ) );
+ add_action( 'delete_post_tag', array( __CLASS__, 'delete_post_tag' ) );
+ add_action( 'customize_controls_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
+ add_action( 'pre_get_posts', array( __CLASS__, 'pre_get_posts' ) );
+ add_action( 'switch_theme', array( __CLASS__, 'switch_theme' ) );
+ add_action( 'switch_theme', array( __CLASS__, 'delete_transient' ) );
+ add_action( 'wp_loaded', array( __CLASS__, 'wp_loaded' ) );
+ add_action( 'update_option_featured-content', array( __CLASS__, 'flush_post_tag_cache' ), 10, 2 );
+ add_action( 'delete_option_featured-content', array( __CLASS__, 'flush_post_tag_cache' ), 10, 2 );
+ add_action( 'split_shared_term', array( __CLASS__, 'jetpack_update_featured_content_for_split_terms', 10, 4 ) );
+
+ if ( isset( $theme_support[0]['additional_post_types'] ) ) {
+ $theme_support[0]['post_types'] = array_merge( array( 'post' ), (array) $theme_support[0]['additional_post_types'] );
+ unset( $theme_support[0]['additional_post_types'] );
+ }
+
+ // Themes can allow Featured Content pages
+ if ( isset( $theme_support[0]['post_types'] ) ) {
+ self::$post_types = array_merge( self::$post_types, (array) $theme_support[0]['post_types'] );
+ self::$post_types = array_unique( self::$post_types );
+
+ // register post_tag support for each post type
+ foreach ( self::$post_types as $post_type ) {
+ register_taxonomy_for_object_type( 'post_tag', $post_type );
+ }
+ }
+ }
+
+ /**
+ * Hide "featured" tag from the front-end.
+ *
+ * Has to run on wp_loaded so that the preview filters of the customizer
+ * have a chance to alter the value.
+ */
+ public static function wp_loaded() {
+ if ( self::get_setting( 'hide-tag' ) ) {
+ $settings = self::get_setting();
+
+ // This is done before setting filters for get_terms in order to avoid an infinite filter loop
+ self::$tag = get_term_by( 'name', $settings['tag-name'], 'post_tag' );
+
+ add_filter( 'get_terms', array( __CLASS__, 'hide_featured_term' ), 10, 3 );
+ add_filter( 'get_the_terms', array( __CLASS__, 'hide_the_featured_term' ), 10, 3 );
+ }
+ }
+
+ /**
+ * Get featured posts
+ *
+ * This method is not intended to be called directly. Theme developers should
+ * place a filter directly in their theme and then pass its name as a value of
+ * the "filter" key in the array passed as the $args parameter during the call
+ * to: add_theme_support( 'featured-content', $args ).
+ *
+ * @uses Featured_Content::get_featured_post_ids()
+ *
+ * @return array
+ */
+ public static function get_featured_posts() {
+ $post_ids = self::get_featured_post_ids();
+
+ // No need to query if there is are no featured posts.
+ if ( empty( $post_ids ) ) {
+ return array();
+ }
+
+ $featured_posts = get_posts(
+ array(
+ 'include' => $post_ids,
+ 'posts_per_page' => count( $post_ids ),
+ 'post_type' => self::$post_types,
+ 'suppress_filters' => false,
+ )
+ );
+
+ return $featured_posts;
+ }
+
+ /**
+ * Get featured post IDs
+ *
+ * This function will return the an array containing the post IDs of all
+ * featured posts.
+ *
+ * Sets the "featured_content_ids" transient.
+ *
+ * @return array Array of post IDs.
+ */
+ public static function get_featured_post_ids() {
+ // Return array of cached results if they exist.
+ $featured_ids = get_transient( 'featured_content_ids' );
+ if ( ! empty( $featured_ids ) ) {
+ return array_map(
+ 'absint',
+ /**
+ * Filter the list of Featured Posts IDs.
+ *
+ * @module theme-tools
+ *
+ * @since 2.7.0
+ *
+ * @param array $featured_ids Array of post IDs.
+ */
+ apply_filters( 'featured_content_post_ids', (array) $featured_ids )
+ );
+ }
+
+ $settings = self::get_setting();
+
+ // Return empty array if no tag name is set.
+ $term = get_term_by( 'name', $settings['tag-name'], 'post_tag' );
+ if ( ! $term ) {
+ $term = get_term_by( 'id', $settings['tag-id'], 'post_tag' );
+ }
+ if ( $term ) {
+ $tag = $term->term_id;
+ } else {
+ /** This action is documented in modules/theme-tools/featured-content.php */
+ return apply_filters( 'featured_content_post_ids', array() );
+ }
+
+ // Back compat for installs that have the quantity option still set.
+ $quantity = isset( $settings['quantity'] ) ? $settings['quantity'] : self::$max_posts;
+
+ // Query for featured posts.
+ $featured = get_posts(
+ array(
+ 'numberposts' => $quantity,
+ 'post_type' => self::$post_types,
+ 'suppress_filters' => false,
+ 'tax_query' => array(
+ array(
+ 'field' => 'term_id',
+ 'taxonomy' => 'post_tag',
+ 'terms' => $tag,
+ ),
+ ),
+ )
+ );
+
+ // Return empty array if no featured content exists.
+ if ( ! $featured ) {
+ /** This action is documented in modules/theme-tools/featured-content.php */
+ return apply_filters( 'featured_content_post_ids', array() );
+ }
+
+ // Ensure correct format before save/return.
+ $featured_ids = wp_list_pluck( (array) $featured, 'ID' );
+ $featured_ids = array_map( 'absint', $featured_ids );
+
+ set_transient( 'featured_content_ids', $featured_ids );
+
+ /** This action is documented in modules/theme-tools/featured-content.php */
+ return apply_filters( 'featured_content_post_ids', $featured_ids );
+ }
+
+ /**
+ * Delete Transient.
+ *
+ * Hooks in the "save_post" action.
+ *
+ * @see Featured_Content::validate_settings().
+ */
+ public static function delete_transient() {
+ delete_transient( 'featured_content_ids' );
+ }
+
+ /**
+ * Flush the Post Tag relationships cache.
+ *
+ * Hooks in the "update_option_featured-content" action.
+ */
+ public static function flush_post_tag_cache( $prev, $opts ) {
+ if ( ! empty( $opts ) && ! empty( $opts['tag-id'] ) ) {
+ $query = new WP_Query(
+ array(
+ 'tag_id' => (int) $opts['tag-id'],
+ 'posts_per_page' => -1,
+ )
+ );
+ foreach ( $query->posts as $post ) {
+ wp_cache_delete( $post->ID, 'post_tag_relationships' );
+ }
+ }
+ }
+
+ /**
+ * Exclude featured posts from the blog query when the blog is the front-page,
+ * and user has not checked the "Also display tagged posts outside the Featured Content area" checkbox.
+ *
+ * Filter the home page posts, and remove any featured post ID's from it.
+ * Hooked onto the 'pre_get_posts' action, this changes the parameters of the
+ * query before it gets any posts.
+ *
+ * @uses Featured_Content::get_featured_post_ids();
+ * @uses Featured_Content::get_setting();
+ * @param WP_Query $query
+ * @return WP_Query Possibly modified WP_Query
+ */
+ public static function pre_get_posts( $query ) {
+
+ // Bail if not home or not main query.
+ if ( ! $query->is_home() || ! $query->is_main_query() ) {
+ return;
+ }
+
+ // Bail if the blog page is not the front page.
+ if ( 'posts' !== get_option( 'show_on_front' ) ) {
+ return;
+ }
+
+ $featured = self::get_featured_post_ids();
+
+ // Bail if no featured posts.
+ if ( ! $featured ) {
+ return;
+ }
+
+ $settings = self::get_setting();
+
+ // Bail if the user wants featured posts always displayed.
+ if ( true == $settings['show-all'] ) {
+ return;
+ }
+
+ // We need to respect post ids already in the blacklist.
+ $post__not_in = $query->get( 'post__not_in' );
+
+ if ( ! empty( $post__not_in ) ) {
+ $featured = array_merge( (array) $post__not_in, $featured );
+ $featured = array_unique( $featured );
+ }
+
+ $query->set( 'post__not_in', $featured );
+ }
+
+ /**
+ * Reset tag option when the saved tag is deleted.
+ *
+ * It's important to mention that the transient needs to be deleted, too.
+ * While it may not be obvious by looking at the function alone, the transient
+ * is deleted by Featured_Content::validate_settings().
+ *
+ * Hooks in the "delete_post_tag" action.
+ *
+ * @see Featured_Content::validate_settings().
+ *
+ * @param int $tag_id The term_id of the tag that has been deleted.
+ * @return void
+ */
+ public static function delete_post_tag( $tag_id ) {
+ $settings = self::get_setting();
+
+ if ( empty( $settings['tag-id'] ) || $tag_id != $settings['tag-id'] ) {
+ return;
+ }
+
+ $settings['tag-id'] = 0;
+ $settings = self::validate_settings( $settings );
+ update_option( 'featured-content', $settings );
+ }
+
+ /**
+ * Hide featured tag from displaying when global terms are queried from
+ * the front-end.
+ *
+ * Hooks into the "get_terms" filter.
+ *
+ * @uses Featured_Content::get_setting()
+ *
+ * @param array $terms A list of term objects. This is the return value of get_terms().
+ * @param array $taxonomies An array of taxonomy slugs.
+ * @return array $terms
+ */
+ public static function hide_featured_term( $terms, $taxonomies, $args ) {
+
+ // This filter is only appropriate on the front-end.
+ if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) ) {
+ return $terms;
+ }
+
+ // We only want to hide the featured tag.
+ if ( ! in_array( 'post_tag', $taxonomies ) ) {
+ return $terms;
+ }
+
+ // Bail if no terms were returned.
+ if ( empty( $terms ) ) {
+ return $terms;
+ }
+
+ // Bail if term objects are unavailable.
+ if ( 'all' != $args['fields'] ) {
+ return $terms;
+ }
+
+ $settings = self::get_setting();
+
+ if ( false !== self::$tag ) {
+ foreach ( $terms as $order => $term ) {
+ if (
+ is_object( $term )
+ && (
+ $settings['tag-id'] === $term->term_id
+ || $settings['tag-name'] === $term->name
+ )
+ ) {
+ unset( $terms[ $order ] );
+ }
+ }
+ }
+
+ return $terms;
+ }
+
+ /**
+ * Hide featured tag from displaying when terms associated with a post object
+ * are queried from the front-end.
+ *
+ * Hooks into the "get_the_terms" filter.
+ *
+ * @uses Featured_Content::get_setting()
+ *
+ * @param array $terms A list of term objects. This is the return value of get_the_terms().
+ * @param int $id The ID field for the post object that terms are associated with.
+ * @param array $taxonomy An array of taxonomy slugs.
+ * @return array $terms
+ */
+ public static function hide_the_featured_term( $terms, $id, $taxonomy ) {
+
+ // This filter is only appropriate on the front-end.
+ if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) ) {
+ return $terms;
+ }
+
+ // Make sure we are in the correct taxonomy.
+ if ( 'post_tag' != $taxonomy ) {
+ return $terms;
+ }
+
+ // No terms? Return early!
+ if ( empty( $terms ) ) {
+ return $terms;
+ }
+
+ $settings = self::get_setting();
+ $tag = get_term_by( 'name', $settings['tag-name'], 'post_tag' );
+
+ if ( false !== $tag ) {
+ foreach ( $terms as $order => $term ) {
+ if ( $settings['tag-id'] === $term->term_id || $settings['tag-name'] === $term->name ) {
+ unset( $terms[ $order ] );
+ }
+ }
+ }
+
+ return $terms;
+ }
+
+ /**
+ * Register custom setting on the Settings -> Reading screen.
+ *
+ * @uses Featured_Content::render_form()
+ * @uses Featured_Content::validate_settings()
+ *
+ * @return void
+ */
+ public static function register_setting() {
+ add_settings_field( 'featured-content', __( 'Featured Content', 'jetpack' ), array( __class__, 'render_form' ), 'reading' );
+
+ // Register sanitization callback for the Customizer.
+ register_setting( 'featured-content', 'featured-content', array( __class__, 'validate_settings' ) );
+ }
+
+ /**
+ * Add settings to the Customizer.
+ *
+ * @param WP_Customize_Manager $wp_customize Theme Customizer object.
+ */
+ public static function customize_register( $wp_customize ) {
+ $wp_customize->add_section(
+ 'featured_content',
+ array(
+ 'title' => esc_html__( 'Featured Content', 'jetpack' ),
+ 'description' => sprintf( __( 'Easily feature all posts with the <a href="%1$s">"featured" tag</a> or a tag of your choice. Your theme supports up to %2$s posts in its featured content area.', 'jetpack' ), admin_url( '/edit.php?tag=featured' ), absint( self::$max_posts ) ),
+ 'priority' => 130,
+ 'theme_supports' => 'featured-content',
+ )
+ );
+
+ /*
+ Add Featured Content settings.
+ *
+ * Sanitization callback registered in Featured_Content::validate_settings().
+ * See http://themeshaper.com/2013/04/29/validation-sanitization-in-customizer/comment-page-1/#comment-12374
+ */
+ $wp_customize->add_setting(
+ 'featured-content[tag-name]',
+ array(
+ 'type' => 'option',
+ 'sanitize_js_callback' => array( __CLASS__, 'delete_transient' ),
+ )
+ );
+ $wp_customize->add_setting(
+ 'featured-content[hide-tag]',
+ array(
+ 'default' => true,
+ 'type' => 'option',
+ 'sanitize_js_callback' => array( __CLASS__, 'delete_transient' ),
+ )
+ );
+ $wp_customize->add_setting(
+ 'featured-content[show-all]',
+ array(
+ 'default' => false,
+ 'type' => 'option',
+ 'sanitize_js_callback' => array( __CLASS__, 'delete_transient' ),
+ )
+ );
+
+ // Add Featured Content controls.
+ $wp_customize->add_control(
+ 'featured-content[tag-name]',
+ array(
+ 'label' => esc_html__( 'Tag name', 'jetpack' ),
+ 'section' => 'featured_content',
+ 'theme_supports' => 'featured-content',
+ 'priority' => 20,
+ )
+ );
+ $wp_customize->add_control(
+ 'featured-content[hide-tag]',
+ array(
+ 'label' => esc_html__( 'Do not display tag in post details and tag clouds.', 'jetpack' ),
+ 'section' => 'featured_content',
+ 'theme_supports' => 'featured-content',
+ 'type' => 'checkbox',
+ 'priority' => 30,
+ )
+ );
+ $wp_customize->add_control(
+ 'featured-content[show-all]',
+ array(
+ 'label' => esc_html__( 'Also display tagged posts outside the Featured Content area.', 'jetpack' ),
+ 'section' => 'featured_content',
+ 'theme_supports' => 'featured-content',
+ 'type' => 'checkbox',
+ 'priority' => 40,
+ )
+ );
+ }
+
+ /**
+ * Enqueue the tag suggestion script.
+ */
+ public static function enqueue_scripts() {
+ wp_enqueue_script( 'featured-content-suggest', plugins_url( 'js/suggest.js', __FILE__ ), array( 'suggest' ), '20131022', true );
+ }
+
+ /**
+ * Renders all form fields on the Settings -> Reading screen.
+ */
+ public static function render_form() {
+ printf( __( 'The settings for Featured Content have <a href="%s">moved to Appearance &rarr; Customize</a>.', 'jetpack' ), admin_url( 'customize.php?#accordion-section-featured_content' ) );
+ }
+
+ /**
+ * Get settings
+ *
+ * Get all settings recognized by this module. This function will return all
+ * settings whether or not they have been stored in the database yet. This
+ * ensures that all keys are available at all times.
+ *
+ * In the event that you only require one setting, you may pass its name as the
+ * first parameter to the function and only that value will be returned.
+ *
+ * @param string $key The key of a recognized setting.
+ * @return mixed Array of all settings by default. A single value if passed as first parameter.
+ */
+ public static function get_setting( $key = 'all' ) {
+ $saved = (array) get_option( 'featured-content' );
+
+ /**
+ * Filter Featured Content's default settings.
+ *
+ * @module theme-tools
+ *
+ * @since 2.7.0
+ *
+ * @param array $args {
+ * Array of Featured Content Settings
+ *
+ * @type int hide-tag Default is 1.
+ * @type int tag-id Default is 0.
+ * @type string tag-name Default is empty.
+ * @type int show-all Default is 0.
+ * }
+ */
+ $defaults = apply_filters(
+ 'featured_content_default_settings',
+ array(
+ 'hide-tag' => 1,
+ 'tag-id' => 0,
+ 'tag-name' => '',
+ 'show-all' => 0,
+ )
+ );
+
+ $options = wp_parse_args( $saved, $defaults );
+ $options = array_intersect_key( $options, $defaults );
+
+ if ( 'all' != $key ) {
+ return isset( $options[ $key ] ) ? $options[ $key ] : false;
+ }
+
+ return $options;
+ }
+
+ /**
+ * Validate settings
+ *
+ * Make sure that all user supplied content is in an expected format before
+ * saving to the database. This function will also delete the transient set in
+ * Featured_Content::get_featured_content().
+ *
+ * @uses Featured_Content::delete_transient()
+ *
+ * @param array $input
+ * @return array $output
+ */
+ public static function validate_settings( $input ) {
+ $output = array();
+
+ if ( empty( $input['tag-name'] ) ) {
+ $output['tag-id'] = 0;
+ } else {
+ $term = get_term_by( 'name', $input['tag-name'], 'post_tag' );
+
+ if ( $term ) {
+ $output['tag-id'] = $term->term_id;
+ } else {
+ $new_tag = wp_create_tag( $input['tag-name'] );
+
+ if ( ! is_wp_error( $new_tag ) && isset( $new_tag['term_id'] ) ) {
+ $output['tag-id'] = $new_tag['term_id'];
+ }
+ }
+
+ $output['tag-name'] = $input['tag-name'];
+ }
+
+ $output['hide-tag'] = isset( $input['hide-tag'] ) && $input['hide-tag'] ? 1 : 0;
+
+ $output['show-all'] = isset( $input['show-all'] ) && $input['show-all'] ? 1 : 0;
+
+ self::delete_transient();
+
+ return $output;
+ }
+
+ /**
+ * Removes the quantity setting from the options array.
+ *
+ * @return void
+ */
+ public static function switch_theme() {
+ $option = (array) get_option( 'featured-content' );
+
+ if ( isset( $option['quantity'] ) ) {
+ unset( $option['quantity'] );
+ update_option( 'featured-content', $option );
+ }
+ }
+
+ public static function jetpack_update_featured_content_for_split_terms( $old_term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
+ $featured_content_settings = get_option( 'featured-content', array() );
+
+ // Check to see whether the stored tag ID is the one that's just been split.
+ if ( isset( $featured_content_settings['tag-id'] ) && $old_term_id == $featured_content_settings['tag-id'] && 'post_tag' == $taxonomy ) {
+ // We have a match, so we swap out the old tag ID for the new one and resave the option.
+ $featured_content_settings['tag-id'] = $new_term_id;
+ update_option( 'featured-content', $featured_content_settings );
+ }
+ }
+ }
+
+ /**
+ * Adds the featured content plugin to the set of files for which action
+ * handlers should be copied when the theme context is loaded by the REST API.
+ *
+ * @param array $copy_dirs Copy paths with actions to be copied
+ * @return array Copy paths with featured content plugin
+ */
+ function wpcom_rest_api_featured_content_copy_plugin_actions( $copy_dirs ) {
+ $copy_dirs[] = __FILE__;
+ return $copy_dirs;
+ }
+ add_action( 'restapi_theme_action_copy_dirs', 'wpcom_rest_api_featured_content_copy_plugin_actions' );
+
+ /**
+ * Delayed initialization for API Requests.
+ */
+ function wpcom_rest_request_before_callbacks( $request ) {
+ Featured_Content::init();
+ return $request;
+ }
+
+ if ( Jetpack_Constants::is_true( 'IS_WPCOM' ) && Jetpack_Constants::is_true( 'REST_API_REQUEST' ) ) {
+ add_filter( 'rest_request_before_callbacks', 'wpcom_rest_request_before_callbacks');
+ }
+
+ Featured_Content::setup();
+} // end if ( ! class_exists( 'Featured_Content' ) && isset( $GLOBALS['pagenow'] ) && 'plugins.php' !== $GLOBALS['pagenow'] ) {
diff --git a/plugins/jetpack/modules/theme-tools/infinite-scroll.php b/plugins/jetpack/modules/theme-tools/infinite-scroll.php
new file mode 100644
index 00000000..ac5a0aa8
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/infinite-scroll.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * INFINITE SCROLL
+ */
+
+/**
+ * Load theme's infinite scroll annotation file, if present in the IS plugin.
+ * The `setup_theme` action is used because the annotation files should be using `after_setup_theme` to register support for IS.
+ *
+ * As released in Jetpack 2.0, a child theme's parent wasn't checked for in the plugin's bundled support, hence the convoluted way the parent is checked for now.
+ *
+ * @uses is_admin, wp_get_theme, apply_filters
+ * @action setup_theme
+ * @return null
+ */
+function jetpack_load_infinite_scroll_annotation() {
+ if ( is_admin() && isset( $_GET['page'] ) && 'jetpack' == $_GET['page'] ) {
+ $theme = wp_get_theme();
+
+ if ( ! is_a( $theme, 'WP_Theme' ) && ! is_array( $theme ) ) {
+ return;
+ }
+
+ /** This filter is already documented in modules/infinite-scroll/infinity.php */
+ $customization_file = apply_filters( 'infinite_scroll_customization_file', dirname( __FILE__ ) . "/infinite-scroll/themes/{$theme['Stylesheet']}.php", $theme['Stylesheet'] );
+
+ if ( is_readable( $customization_file ) ) {
+ require_once $customization_file;
+ } elseif ( ! empty( $theme['Template'] ) ) {
+ $customization_file = dirname( __FILE__ ) . "/infinite-scroll/themes/{$theme['Template']}.php";
+
+ if ( is_readable( $customization_file ) ) {
+ require_once $customization_file;
+ }
+ }
+ }
+}
+add_action( 'setup_theme', 'jetpack_load_infinite_scroll_annotation' );
+
+/**
+ * Prevent IS from being activated if theme doesn't support it
+ *
+ * @param bool $can_activate
+ * @filter jetpack_can_activate_infinite-scroll
+ * @return bool
+ */
+function jetpack_can_activate_infinite_scroll() {
+ return (bool) current_theme_supports( 'infinite-scroll' );
+}
+add_filter( 'jetpack_can_activate_infinite-scroll', 'jetpack_can_activate_infinite_scroll' );
diff --git a/plugins/jetpack/modules/theme-tools/js/suggest.js b/plugins/jetpack/modules/theme-tools/js/suggest.js
new file mode 100644
index 00000000..4d6c8794
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/js/suggest.js
@@ -0,0 +1,7 @@
+/* global ajaxurl:true */
+jQuery( function( $ ) {
+ $( '#customize-control-featured-content-tag-name input' ).suggest(
+ ajaxurl + '?action=ajax-tag-search&tax=post_tag',
+ { delay: 500, minchars: 2 }
+ );
+} );
diff --git a/plugins/jetpack/modules/theme-tools/random-redirect.php b/plugins/jetpack/modules/theme-tools/random-redirect.php
new file mode 100644
index 00000000..d866d336
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/random-redirect.php
@@ -0,0 +1,83 @@
+<?php
+/*
+Plugin Name: Random Redirect
+Plugin URI: https://wordpress.org/extend/plugins/random-redirect/
+Description: Allows you to create a link to yourblog.example.com/?random which will redirect someone to a random post on your blog, in a StumbleUpon-like fashion.
+Version: 1.2-wpcom
+Author: Matt Mullenweg
+Author URI: http://photomatt.net/
+*/
+
+function jetpack_matt_random_redirect() {
+ // Verify that the Random Redirect plugin this code is from is not active
+ // See http://plugins.trac.wordpress.org/ticket/1898
+ if ( ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ if ( is_plugin_active( 'random-redirect/random-redirect.php' ) ) {
+ return;
+ }
+ }
+
+ // Set default post type.
+ $post_type = get_post_type();
+
+ // Set default category type
+ if ( is_category() ) {
+ $category = get_the_category();
+ if ( isset( $category ) && ! empty( $category ) ) {
+ $random_cat_id = $category[0]->term_id;
+ }
+ }
+
+ // Set author name if we're on an author archive.
+ if ( is_author() ) {
+ $random_author_name = get_the_author_meta( 'user_login' );
+ $random_author_query = 'AND user_login = "' . $random_author_name . '"';
+ } else {
+ $random_author_query = '';
+ }
+
+ // Acceptable URL formats: /[...]/?random=[post type], /?random, /&random, /&random=1
+ if ( ! isset( $_GET['random'] ) && ! in_array( strtolower( $_SERVER['REQUEST_URI'] ), array( '/&random', '/&random=1' ) ) ) {
+ return;
+ }
+
+ // Ignore POST requests.
+ if ( ! empty( $_POST ) ) {
+ return;
+ }
+
+ // Persistent AppEngine abuse. ORDER BY RAND is expensive.
+ if ( strstr( $_SERVER['HTTP_USER_AGENT'], 'AppEngine-Google' ) ) {
+ wp_die( 'Please <a href="http://en.support.wordpress.com/contact/" rel="noopener noreferrer" target="_blank">contact support</a>' );
+ }
+
+ // Set the category ID if the parameter is set.
+ if ( isset( $_GET['random_cat_id'] ) ) {
+ $random_cat_id = (int) $_GET['random_cat_id'];
+ }
+
+ // Change the post type if the parameter is set.
+ if ( isset( $_GET['random_post_type'] ) && post_type_exists( $_GET['random_post_type'] ) ) {
+ $post_type = $_GET['random_post_type'];
+ }
+
+ // Don't show a random page if 'page' isn't specified as the post type specifically.
+ if ( 'page' === $post_type && is_front_page() && ! isset( $_GET['random_post_type'] ) ) {
+ $post_type = 'post';
+ }
+
+ global $wpdb;
+
+ if ( isset( $random_cat_id ) ) {
+ $random_id = $wpdb->get_var( $wpdb->prepare( "SELECT DISTINCT ID FROM $wpdb->posts AS p INNER JOIN $wpdb->term_relationships AS tr ON (p.ID = tr.object_id AND tr.term_taxonomy_id = %s) INNER JOIN $wpdb->term_taxonomy AS tt ON(tr.term_taxonomy_id = tt.term_taxonomy_id AND taxonomy = 'category') WHERE p.post_type = %s AND post_password = '' AND post_status = 'publish' %s ORDER BY RAND() LIMIT 1", $random_cat_id, $post_type, $random_author_query ) );
+ } else {
+ $random_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = %s AND post_password = '' AND post_status = 'publish' %s ORDER BY RAND() LIMIT 1", $post_type, $random_author_query ) );
+ }
+
+ $permalink = get_permalink( $random_id );
+ wp_safe_redirect( $permalink );
+ exit;
+}
+
+add_action( 'template_redirect', 'jetpack_matt_random_redirect' );
diff --git a/plugins/jetpack/modules/theme-tools/responsive-videos.php b/plugins/jetpack/modules/theme-tools/responsive-videos.php
new file mode 100644
index 00000000..61987a8d
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/responsive-videos.php
@@ -0,0 +1,152 @@
+<?php
+
+/**
+ * Load the Responsive videos plugin
+ */
+function jetpack_responsive_videos_init() {
+
+ /* If the doesn't theme support 'jetpack-responsive-videos', don't continue */
+ if ( ! current_theme_supports( 'jetpack-responsive-videos' ) ) {
+ return;
+ }
+
+ /* If the theme does support 'jetpack-responsive-videos', wrap the videos */
+ add_filter( 'wp_video_shortcode', 'jetpack_responsive_videos_embed_html' );
+ add_filter( 'video_embed_html', 'jetpack_responsive_videos_embed_html' );
+
+ /* Only wrap oEmbeds if video */
+ add_filter( 'embed_oembed_html', 'jetpack_responsive_videos_maybe_wrap_oembed', 10, 2 );
+ add_filter( 'embed_handler_html', 'jetpack_responsive_videos_maybe_wrap_oembed', 10, 2 );
+
+ /* Wrap videos in Buddypress */
+ add_filter( 'bp_embed_oembed_html', 'jetpack_responsive_videos_embed_html' );
+
+ /* Wrap Slideshare shortcodes */
+ add_filter( 'jetpack_slideshare_shortcode', 'jetpack_responsive_videos_embed_html' );
+
+ // Remove the Jetpack Responsive video wrapper in embed blocks on sites that support core Responsive embeds.
+ if ( current_theme_supports( 'responsive-embeds' ) ) {
+ add_filter( 'render_block', 'jetpack_responsive_videos_remove_wrap_oembed', 10, 2 );
+ }
+}
+add_action( 'after_setup_theme', 'jetpack_responsive_videos_init', 99 );
+
+
+/**
+ * Adds a wrapper to videos and enqueue script
+ *
+ * @return string
+ */
+function jetpack_responsive_videos_embed_html( $html ) {
+ if ( empty( $html ) || ! is_string( $html ) ) {
+ return $html;
+ }
+
+ // The customizer video widget wraps videos with a class of wp-video
+ // mejs as of 4.9 apparently resizes videos too which causes issues
+ // skip the video if it is wrapped in wp-video.
+ $video_widget_wrapper = 'class="wp-video"';
+
+ $mejs_wrapped = strpos( $html, $video_widget_wrapper );
+
+ // If this is a video widget wrapped by mejs, return the html.
+ if ( false !== $mejs_wrapped ) {
+ return $html;
+ }
+
+ if ( defined( 'SCRIPT_DEBUG' ) && true == SCRIPT_DEBUG ) {
+ wp_enqueue_script( 'jetpack-responsive-videos-script', plugins_url( 'responsive-videos/responsive-videos.js', __FILE__ ), array( 'jquery' ), '1.3', true );
+ } else {
+ wp_enqueue_script( 'jetpack-responsive-videos-min-script', plugins_url( 'responsive-videos/responsive-videos.min.js', __FILE__ ), array( 'jquery' ), '1.3', true );
+ }
+
+ // Enqueue CSS to ensure compatibility with all themes
+ wp_enqueue_style( 'jetpack-responsive-videos-style', plugins_url( 'responsive-videos/responsive-videos.css', __FILE__ ) );
+
+ return '<div class="jetpack-video-wrapper">' . $html . '</div>';
+}
+
+/**
+ * Check if oEmbed is a `$video_patterns` provider video before wrapping.
+ *
+ * @param mixed $html The cached HTML result, stored in post meta.
+ * @param string $url he attempted embed URL.
+ *
+ * @return string
+ */
+function jetpack_responsive_videos_maybe_wrap_oembed( $html, $url = null ) {
+ if ( empty( $html ) || ! is_string( $html ) || ! $url ) {
+ return $html;
+ }
+
+ $jetpack_video_wrapper = '<div class="jetpack-video-wrapper">';
+
+ $already_wrapped = strpos( $html, $jetpack_video_wrapper );
+
+ // If the oEmbed has already been wrapped, return the html.
+ if ( false !== $already_wrapped ) {
+ return $html;
+ }
+
+ /**
+ * oEmbed Video Providers.
+ *
+ * A whitelist of oEmbed video provider Regex patterns to check against before wrapping the output.
+ *
+ * @module theme-tools
+ *
+ * @since 3.8.0
+ *
+ * @param array $video_patterns oEmbed video provider Regex patterns.
+ */
+ $video_patterns = apply_filters(
+ 'jetpack_responsive_videos_oembed_videos',
+ array(
+ 'https?://((m|www)\.)?youtube\.com/watch',
+ 'https?://((m|www)\.)?youtube\.com/playlist',
+ 'https?://youtu\.be/',
+ 'https?://(.+\.)?vimeo\.com/',
+ 'https?://(www\.)?dailymotion\.com/',
+ 'https?://dai.ly/',
+ 'https?://(www\.)?hulu\.com/watch/',
+ 'https?://wordpress.tv/',
+ 'https?://(www\.)?funnyordie\.com/videos/',
+ 'https?://vine.co/v/',
+ 'https?://(www\.)?collegehumor\.com/video/',
+ 'https?://(www\.|embed\.)?ted\.com/talks/',
+ )
+ );
+
+ // Merge patterns to run in a single preg_match call.
+ $video_patterns = '(' . implode( '|', $video_patterns ) . ')';
+
+ $is_video = preg_match( $video_patterns, $url );
+
+ // If the oEmbed is a video, wrap it in the responsive wrapper.
+ if ( false === $already_wrapped && 1 === $is_video ) {
+ return jetpack_responsive_videos_embed_html( $html );
+ }
+
+ return $html;
+}
+
+/**
+ * Remove the Jetpack Responsive video wrapper in embed blocks.
+ *
+ * @since 7.0.0
+ *
+ * @param string $block_content The block content about to be appended.
+ * @param array $block The full block, including name and attributes.
+ *
+ * @return string $block_content String of rendered HTML.
+ */
+function jetpack_responsive_videos_remove_wrap_oembed( $block_content, $block ) {
+ if (
+ isset( $block['blockName'] )
+ && false !== strpos( $block['blockName'], 'core-embed' )
+ ) {
+ $block_content = preg_replace( '#<div class="jetpack-video-wrapper">(.*?)</div>#', '${1}', $block_content );
+ }
+
+ return $block_content;
+}
diff --git a/plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.css b/plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.css
new file mode 100644
index 00000000..056f1829
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.css
@@ -0,0 +1,10 @@
+.jetpack-video-wrapper {
+ margin-bottom: 1.6em;
+}
+
+.jetpack-video-wrapper > embed,
+.jetpack-video-wrapper > iframe,
+.jetpack-video-wrapper > object,
+.jetpack-video-wrapper > .wp-video {
+ margin-bottom: 0;
+}
diff --git a/plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.js b/plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.js
new file mode 100644
index 00000000..668cc7a0
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.js
@@ -0,0 +1,63 @@
+( function( $ ) {
+ var resizeTimer;
+
+ function responsiveVideos() {
+ $( '.jetpack-video-wrapper' )
+ .find( 'embed, iframe, object' )
+ .each( function() {
+ var _this, videoWidth, videoHeight, videoRatio, videoWrapper, videoMargin, containerWidth;
+
+ _this = $( this );
+ videoMargin = 0;
+
+ if (
+ _this
+ .parents( '.jetpack-video-wrapper' )
+ .prev( 'p' )
+ .css( 'text-align' ) === 'center'
+ ) {
+ videoMargin = '0 auto';
+ }
+
+ if ( ! _this.attr( 'data-ratio' ) ) {
+ _this
+ .attr( 'data-ratio', this.height / this.width )
+ .attr( 'data-width', this.width )
+ .attr( 'data-height', this.height )
+ .css( {
+ display: 'block',
+ margin: videoMargin,
+ } );
+ }
+
+ videoWidth = _this.attr( 'data-width' );
+ videoHeight = _this.attr( 'data-height' );
+ videoRatio = _this.attr( 'data-ratio' );
+ videoWrapper = _this.parent();
+ containerWidth = videoWrapper.width();
+
+ if ( videoRatio === 'Infinity' ) {
+ videoWidth = '100%';
+ }
+
+ _this.removeAttr( 'height' ).removeAttr( 'width' );
+
+ if ( videoWidth > containerWidth ) {
+ _this.width( containerWidth ).height( containerWidth * videoRatio );
+ } else {
+ _this.width( videoWidth ).height( videoHeight );
+ }
+ } );
+ }
+
+ $( document ).ready( function() {
+ $( window )
+ .on( 'load.jetpack', responsiveVideos )
+ .on( 'resize.jetpack', function() {
+ clearTimeout( resizeTimer );
+ resizeTimer = setTimeout( responsiveVideos, 500 );
+ } )
+ .on( 'post-load.jetpack', responsiveVideos )
+ .resize();
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.min.js b/plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.min.js
new file mode 100644
index 00000000..ff419935
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/responsive-videos/responsive-videos.min.js
@@ -0,0 +1 @@
+!function(t){function a(){t(".jetpack-video-wrapper").find("embed, iframe, object").each(function(){var a,e,i,r,h,d,o;a=t(this),d=0,"center"===a.parents(".jetpack-video-wrapper").prev("p").css("text-align")&&(d="0 auto"),a.attr("data-ratio")||a.attr("data-ratio",this.height/this.width).attr("data-width",this.width).attr("data-height",this.height).css({display:"block",margin:d}),e=a.attr("data-width"),i=a.attr("data-height"),r=a.attr("data-ratio"),h=a.parent(),o=h.width(),"Infinity"===r&&(e="100%"),a.removeAttr("height").removeAttr("width"),e>o?a.width(o).height(o*r):a.width(e).height(i)})}var e;t(document).ready(function(){t(window).on("load.jetpack",a).on("resize.jetpack",function(){clearTimeout(e),e=setTimeout(a,500)}).on("post-load.jetpack",a).resize()})}(jQuery); \ No newline at end of file
diff --git a/plugins/jetpack/modules/theme-tools/site-breadcrumbs.php b/plugins/jetpack/modules/theme-tools/site-breadcrumbs.php
new file mode 100644
index 00000000..2f266738
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-breadcrumbs.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Plugin Name: Site Breadcrumbs
+ * Plugin URI: https://wordpress.com
+ * Description: Quickly add breadcrumbs to the single view of a hierarchical post type or a hierarchical taxonomy.
+ * Author: Automattic
+ * Version: 1.0
+ * Author URI: https://wordpress.com
+ * License: GPL2 or later
+ */
+
+function jetpack_breadcrumbs() {
+ $taxonomy = is_category() ? 'category' : get_query_var( 'taxonomy' );
+ $is_taxonomy_hierarchical = is_taxonomy_hierarchical( $taxonomy );
+
+ $post_type = is_page() ? 'page' : get_query_var( 'post_type' );
+ $is_post_type_hierarchical = is_post_type_hierarchical( $post_type );
+
+ if ( ! ( $is_post_type_hierarchical || $is_taxonomy_hierarchical ) || is_front_page() ) {
+ return;
+ }
+
+ $breadcrumb = '';
+
+ if ( $is_post_type_hierarchical ) {
+ $post_id = get_queried_object_id();
+ $ancestors = array_reverse( get_post_ancestors( $post_id ) );
+ if ( $ancestors ) {
+ foreach ( $ancestors as $ancestor ) {
+ $breadcrumb .= '<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><a href="' . esc_url( get_permalink( $ancestor ) ) . '" itemprop="item"><span itemprop="name">' . esc_html( get_the_title( $ancestor ) ) . '</span></a></span>';
+ }
+ }
+ $breadcrumb .= '<span class="current-page" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><span itemprop="name">' . esc_html( get_the_title( $post_id ) ) . '</span></span>';
+ } elseif ( $is_taxonomy_hierarchical ) {
+ $current = get_term( get_queried_object_id(), $taxonomy );
+
+ if ( is_wp_error( $current ) ) {
+ return;
+ }
+
+ if ( $current->parent ) {
+ $breadcrumb = jetpack_get_term_parents( $current->parent, $taxonomy );
+ }
+
+ $breadcrumb .= '<span class="current-category" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><span itemprop="name">' . esc_html( $current->name ) . '</span></span>';
+ }
+
+ $home = '<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><a href="' . esc_url( home_url( '/' ) ) . '" class="home-link" itemprop="item" rel="home"><span itemprop="name">' . esc_html__( 'Home', 'jetpack' ) . '</span></a></span>';
+
+ echo '<nav class="entry-breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">' . $home . $breadcrumb . '</nav>';
+}
+
+/**
+ * Return the parents for a given taxonomy term ID.
+ *
+ * @param int $term Taxonomy term whose parents will be returned.
+ * @param string $taxonomy Taxonomy name that the term belongs to.
+ * @param array $visited Terms already added to prevent duplicates.
+ *
+ * @return string A list of links to the term parents.
+ */
+function jetpack_get_term_parents( $term, $taxonomy, $visited = array() ) {
+ $parent = get_term( $term, $taxonomy );
+
+ if ( is_wp_error( $parent ) ) {
+ return $parent;
+ }
+
+ $chain = '';
+
+ if ( $parent->parent && ( $parent->parent != $parent->term_id ) && ! in_array( $parent->parent, $visited ) ) {
+ $visited[] = $parent->parent;
+ $chain .= jetpack_get_term_parents( $parent->parent, $taxonomy, $visited );
+ }
+
+ $chain .= '<a href="' . esc_url( get_category_link( $parent->term_id ) ) . '">' . $parent->name . '</a>';
+
+ return $chain;
+}
diff --git a/plugins/jetpack/modules/theme-tools/site-logo.php b/plugins/jetpack/modules/theme-tools/site-logo.php
new file mode 100644
index 00000000..04f16b71
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo.php
@@ -0,0 +1,46 @@
+<?php
+/*
+ * Site Logo.
+ * @see http://jetpack.com/support/site-logo/
+ *
+ * This feature will only be activated for themes that declare their support.
+ * This can be done by adding code similar to the following during the
+ * 'after_setup_theme' action:
+ *
+ * $args = array(
+ * 'header-text' => array(
+ * 'site-title',
+ * 'site-description',
+ * ),
+ * 'size' => 'medium',
+ * );
+ * add_theme_support( 'site-logo', $args );
+ *
+ */
+
+/**
+ * Activate the Site Logo plugin.
+ *
+ * @uses current_theme_supports()
+ * @since 3.2
+ */
+function site_logo_init() {
+ // For transferring existing site logo from Jetpack -> Core
+ if ( current_theme_supports( 'custom-logo' ) && ! get_theme_mod( 'custom_logo' ) && $jp_logo = get_option( 'site_logo' ) ) {
+ set_theme_mod( 'custom_logo', $jp_logo['id'] );
+ delete_option( 'site_logo' );
+ }
+
+ // Only load our code if our theme declares support, and the standalone plugin is not activated.
+ if ( current_theme_supports( 'site-logo' ) && ! class_exists( 'Site_Logo', false ) ) {
+ // Load our class for namespacing.
+ require dirname( __FILE__ ) . '/site-logo/inc/class-site-logo.php';
+
+ // Load template tags.
+ require dirname( __FILE__ ) . '/site-logo/inc/functions.php';
+
+ // Load backwards-compatible template tags.
+ require dirname( __FILE__ ) . '/site-logo/inc/compat.php';
+ }
+}
+add_action( 'init', 'site_logo_init' );
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control-rtl.css b/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control-rtl.css
new file mode 100644
index 00000000..d5441db1
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control-rtl.css
@@ -0,0 +1,12 @@
+/**
+ * RTL styles for the Site Logo control. Just swaps the button sides.
+ */
+#customize-control-site_logo .remove {
+ float: right;
+ margin-left: 3px;
+}
+
+#customize-control-site_logo .new,
+#customize-control-site_logo .change {
+ float: left;
+}
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control-rtl.min.css b/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control-rtl.min.css
new file mode 100644
index 00000000..1893fe9c
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control-rtl.min.css
@@ -0,0 +1 @@
+#customize-control-site_logo .remove{float:right;margin-left:3px}#customize-control-site_logo .change,#customize-control-site_logo .new{float:left} \ No newline at end of file
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control.css b/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control.css
new file mode 100644
index 00000000..b9a10fe6
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control.css
@@ -0,0 +1,49 @@
+/**
+ * Styles for the Site Logo control.
+ */
+#customize-control-site_logo .current {
+ margin-bottom: 6px;
+}
+
+#customize-control-site_logo .current span {
+ border: 1px solid #eee;
+ -webkit-border-radius: 2px;
+ border-radius: 2px;
+ color: #555;
+ display: block;
+ overflow: hidden;
+ line-height: 40px;
+ min-height: 40px;
+ padding: 0 6px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+#customize-control-site_logo .current img {
+ max-width: 100%;
+}
+
+#customize-control-site_logo button.new,
+#customize-control-site_logo button.change,
+#customize-control-site_logo button.remove {
+ height: auto;
+ width: 48%;
+ white-space: normal;
+}
+
+#customize-control-site_logo .remove {
+ float: left;
+ margin-right: 3px;
+}
+
+#customize-control-site_logo .new,
+#customize-control-site_logo .change {
+ float: right;
+}
+
+#customize-control-site_logo .customize-control-description {
+ display: block;
+ clear: both;
+ margin-bottom: 10px;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control.min.css b/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control.min.css
new file mode 100644
index 00000000..7fcee5ae
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/css/site-logo-control.min.css
@@ -0,0 +1 @@
+#customize-control-site_logo .current{margin-bottom:6px}#customize-control-site_logo .current span{border:1px solid #eee;-webkit-border-radius:2px;border-radius:2px;color:#555;display:block;overflow:hidden;line-height:40px;min-height:40px;padding:0 6px;text-align:center;text-overflow:ellipsis;white-space:nowrap}#customize-control-site_logo .current img{max-width:100%}#customize-control-site_logo button.change,#customize-control-site_logo button.new,#customize-control-site_logo button.remove{height:auto;width:48%;white-space:normal}#customize-control-site_logo .remove{float:left;margin-right:3px}#customize-control-site_logo .change,#customize-control-site_logo .new{float:right}#customize-control-site_logo .customize-control-description{display:block;clear:both;margin-bottom:10px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/inc/class-site-logo-control.php b/plugins/jetpack/modules/theme-tools/site-logo/inc/class-site-logo-control.php
new file mode 100644
index 00000000..5e23507d
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/inc/class-site-logo-control.php
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Custom logo uploader control for the Customizer.
+ *
+ * @package Jetpack
+ */
+class Site_Logo_Image_Control extends WP_Customize_Control {
+ /**
+ * Constructor for our custom control.
+ *
+ * @param object $wp_customize
+ * @param string $control_id
+ * @param array $args
+ * @uses Site_Logo_Image_Control::l10n()
+ */
+ public function __construct( $wp_customize, $control_id, $args = array() ) {
+ // declare these first so they can be overridden
+ $this->l10n = array(
+ 'upload' => __( 'Add logo', 'jetpack' ),
+ 'set' => __( 'Set as logo', 'jetpack' ),
+ 'choose' => __( 'Choose logo', 'jetpack' ),
+ 'change' => __( 'Change logo', 'jetpack' ),
+ 'remove' => __( 'Remove logo', 'jetpack' ),
+ 'placeholder' => __( 'No logo set', 'jetpack' ),
+ );
+
+ parent::__construct( $wp_customize, $control_id, $args );
+ }
+
+ /**
+ * This will be critical for our JS constructor.
+ */
+ public $type = 'site_logo';
+
+ /**
+ * Allows overriding of global labels by a specific control.
+ */
+ public $l10n = array();
+
+ /**
+ * The type of files that should be allowed by the media modal.
+ */
+ public $mime_type = 'image';
+
+ /**
+ * Enqueue our media manager resources, scripts, and styles.
+ *
+ * @uses wp_enqueue_media()
+ * @uses wp_enqueue_style()
+ * @uses wp_enqueue_script()
+ * @uses plugins_url()
+ */
+ public function enqueue() {
+ // Enqueues all needed media resources.
+ wp_enqueue_media();
+
+ // Enqueue our control script and styles.
+ wp_enqueue_style( 'site-logo-control', plugins_url( '../css/site-logo-control.css', __FILE__ ) );
+ wp_enqueue_script( 'site-logo-control', plugins_url( '../js/site-logo-control.js', __FILE__ ), array( 'media-views', 'customize-controls', 'underscore' ), '', true );
+ }
+
+ /**
+ * Check if we have an active site logo.
+ *
+ * @uses get_option()
+ * @return boolean
+ */
+ public function has_site_logo() {
+ $logo = get_option( 'site_logo' );
+
+ if ( empty( $logo['url'] ) ) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Display our custom control in the Customizer.
+ *
+ * @uses Site_Logo_Image_Control::l10n()
+ * @uses Site_Logo_Image_Control::mime_type()
+ * @uses Site_Logo_Image_Control::label()
+ * @uses Site_Logo_Image_Control::description()
+ * @uses esc_attr()
+ * @uses esc_html()
+ */
+ public function render_content() {
+ // We do this to allow the upload control to specify certain labels
+ $l10n = json_encode( $this->l10n );
+
+ // Control title
+ printf(
+ '<span class="customize-control-title" data-l10n="%s" data-mime="%s">%s</span>',
+ esc_attr( $l10n ),
+ esc_attr( $this->mime_type ),
+ esc_html( $this->label )
+ );
+
+ // Control description
+ if ( ! empty( $this->description ) ) : ?>
+ <span class="description customize-control-description"><?php echo $this->description; ?></span>
+ <?php endif; ?>
+
+ <div class="current"></div>
+ <div class="actions"></div>
+ <?php
+ }
+}
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/inc/class-site-logo.php b/plugins/jetpack/modules/theme-tools/site-logo/inc/class-site-logo.php
new file mode 100644
index 00000000..75fe80b1
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/inc/class-site-logo.php
@@ -0,0 +1,377 @@
+<?php
+/**
+ * Our Site Logo class for managing a theme-agnostic logo through the Customizer.
+ *
+ * @package Jetpack
+ */
+class Site_Logo {
+ /**
+ * Stores our single instance.
+ */
+ private static $instance;
+
+ /**
+ * Stores our current logo settings.
+ */
+ public $logo;
+
+ /**
+ * Return our instance, creating a new one if necessary.
+ *
+ * @uses Site_Logo::$instance
+ * @return object Site_Logo
+ */
+ public static function instance() {
+ if ( ! isset( self::$instance ) ) {
+ self::$instance = new Site_Logo();
+ self::$instance->register_hooks();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Get our current logo settings stored in options.
+ *
+ * @uses get_option()
+ */
+ private function __construct() {
+ $this->logo = get_option( 'site_logo', null );
+ }
+
+ /**
+ * Register our actions and filters.
+ *
+ * @uses Site_Logo::head_text_styles()
+ * @uses Site_Logo::customize_register()
+ * @uses Site_Logo::preview_enqueue()
+ * @uses Site_Logo::body_classes()
+ * @uses Site_Logo::media_manager_image_sizes()
+ * @uses add_action
+ * @uses add_filter
+ */
+ public function register_hooks() {
+ // This would only happen if a theme supports BOTH site-logo and custom-logo for some reason
+ if ( current_theme_supports( 'custom-logo' ) ) {
+ return;
+ }
+
+ add_action( 'wp_head', array( $this, 'head_text_styles' ) );
+ add_action( 'customize_register', array( $this, 'customize_register' ) );
+ add_action( 'customize_preview_init', array( $this, 'preview_enqueue' ) );
+ add_action( 'delete_attachment', array( $this, 'reset_on_attachment_delete' ) );
+ add_filter( 'body_class', array( $this, 'body_classes' ) );
+ add_filter( 'image_size_names_choose', array( $this, 'media_manager_image_sizes' ) );
+ add_filter( 'display_media_states', array( $this, 'add_media_state' ) );
+ }
+
+ /**
+ * Add our logo uploader to the Customizer.
+ *
+ * @param object $wp_customize Customizer object.
+ * @uses current_theme_supports()
+ * @uses current_theme_supports()
+ * @uses WP_Customize_Manager::add_setting()
+ * @uses WP_Customize_Manager::add_control()
+ * @uses Site_Logo::sanitize_checkbox()
+ */
+ public function customize_register( $wp_customize ) {
+ // Include our custom control.
+ require dirname( __FILE__ ) . '/class-site-logo-control.php';
+
+ // Add a setting to hide header text if the theme isn't supporting the feature itself
+ if ( ! current_theme_supports( 'custom-header' ) ) {
+ $wp_customize->add_setting(
+ 'site_logo_header_text',
+ array(
+ 'default' => 1,
+ 'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
+ 'transport' => 'postMessage',
+ )
+ );
+
+ $wp_customize->add_control(
+ new WP_Customize_Control(
+ $wp_customize,
+ 'site_logo_header_text',
+ array(
+ 'label' => __( 'Display Header Text', 'jetpack' ),
+ 'section' => 'title_tagline',
+ 'settings' => 'site_logo_header_text',
+ 'type' => 'checkbox',
+ )
+ )
+ );
+ }
+
+ // Add the setting for our logo value.
+ $wp_customize->add_setting(
+ 'site_logo',
+ array(
+ 'capability' => 'manage_options',
+ 'default' => array(
+ 'id' => 0,
+ 'sizes' => array(),
+ 'url' => false,
+ ),
+ 'sanitize_callback' => array( $this, 'sanitize_logo_setting' ),
+ 'transport' => 'postMessage',
+ 'type' => 'option',
+ )
+ );
+
+ // Add our image uploader.
+ $wp_customize->add_control(
+ new Site_Logo_Image_Control(
+ $wp_customize,
+ 'site_logo',
+ array(
+ 'label' => __( 'Logo', 'jetpack' ),
+ 'section' => 'title_tagline',
+ 'settings' => 'site_logo',
+ )
+ )
+ );
+ }
+
+ /**
+ * Enqueue scripts for the Customizer live preview.
+ *
+ * @uses wp_enqueue_script()
+ * @uses plugins_url()
+ * @uses current_theme_supports()
+ * @uses Site_Logo::header_text_classes()
+ * @uses wp_localize_script()
+ */
+ public function preview_enqueue() {
+ wp_enqueue_script( 'site-logo-preview', plugins_url( '../js/site-logo.js', __FILE__ ), array( 'media-views' ), '', true );
+
+ // Don't bother passing in header text classes if the theme supports custom headers.
+ if ( ! current_theme_supports( 'custom-header' ) ) {
+ $classes = jetpack_sanitize_header_text_classes( $this->header_text_classes() );
+ wp_enqueue_script( 'site-logo-header-text', plugins_url( '../js/site-logo-header-text.js', __FILE__ ), array( 'media-views' ), '', true );
+ wp_localize_script( 'site-logo-header-text', 'site_logo_header_classes', $classes );
+ }
+ }
+
+ /**
+ * Get header text classes. If not defined in add_theme_support(), defaults from Underscores will be used.
+ *
+ * @uses get_theme_support
+ * @return string String of classes to hide
+ */
+ public function header_text_classes() {
+ $args = get_theme_support( 'site-logo' );
+
+ if ( isset( $args[0]['header-text'] ) ) {
+ // Use any classes defined in add_theme_support().
+ $classes = $args[0]['header-text'];
+ } else {
+ // Otherwise, use these defaults, which will work with any Underscores-based theme.
+ $classes = array(
+ 'site-title',
+ 'site-description',
+ );
+ }
+
+ // If we've got an array, reduce them to a string for output
+ if ( is_array( $classes ) ) {
+ $classes = (string) '.' . implode( ', .', $classes );
+ } else {
+ $classes = (string) '.' . $classes;
+ }
+
+ return $classes;
+ }
+
+ /**
+ * Hide header text on front-end if necessary.
+ *
+ * @uses current_theme_supports()
+ * @uses get_theme_mod()
+ * @uses Site_Logo::header_text_classes()
+ * @uses esc_html()
+ */
+ public function head_text_styles() {
+ // Bail if our theme supports custom headers.
+ if ( current_theme_supports( 'custom-header' ) ) {
+ return;
+ }
+
+ // Is Display Header Text unchecked? If so, we need to hide our header text.
+ if ( ! get_theme_mod( 'site_logo_header_text', 1 ) ) {
+ $classes = $this->header_text_classes();
+ ?>
+ <!-- Site Logo: hide header text -->
+ <style type="text/css">
+ <?php echo jetpack_sanitize_header_text_classes( $classes ); ?> {
+ position: absolute;
+ clip: rect(1px, 1px, 1px, 1px);
+ }
+ </style>
+ <?php
+ }
+ }
+
+ /**
+ * Determine image size to use for the logo.
+ *
+ * @uses get_theme_support()
+ * @return string Size specified in add_theme_support declaration, or 'thumbnail' default
+ */
+ public function theme_size() {
+ $args = get_theme_support( 'site-logo' );
+ $valid_sizes = get_intermediate_image_sizes();
+
+ // Add 'full' to the list of accepted values.
+ $valid_sizes[] = 'full';
+
+ // If the size declared in add_theme_support is valid, use it; otherwise, just go with 'thumbnail'.
+ $size = ( isset( $args[0]['size'] ) && in_array( $args[0]['size'], $valid_sizes ) ) ? $args[0]['size'] : 'thumbnail';
+
+ return $size;
+ }
+
+ /**
+ * Make custom image sizes available to the media manager.
+ *
+ * @param array $sizes
+ * @uses get_intermediate_image_sizes()
+ * @return array All default and registered custom image sizes.
+ */
+ public function media_manager_image_sizes( $sizes ) {
+ // Get an array of all registered image sizes.
+ $intermediate = get_intermediate_image_sizes();
+
+ // Have we got anything fun to work with?
+ if ( is_array( $intermediate ) && ! empty( $intermediate ) ) {
+ foreach ( $intermediate as $key => $size ) {
+ // If the size isn't already in the $sizes array, add it.
+ if ( ! array_key_exists( $size, $sizes ) ) {
+ $sizes[ $size ] = $size;
+ }
+ }
+ }
+
+ return $sizes;
+ }
+
+ /**
+ * Add site logos to media states in the Media Manager.
+ *
+ * @return array The current attachment's media states.
+ */
+ public function add_media_state( $media_states ) {
+ // Only bother testing if we have a site logo set.
+ if ( $this->has_site_logo() ) {
+ global $post;
+
+ // If our attachment ID and the site logo ID match, this image is the site logo.
+ if ( $post->ID == $this->logo['id'] ) {
+ $media_states[] = __( 'Site Logo', 'jetpack' );
+ }
+ }
+
+ return $media_states;
+ }
+
+ /**
+ * Reset the site logo if the current logo is deleted in the media manager.
+ *
+ * @param int $site_id
+ * @uses Site_Logo::remove_site_logo()
+ */
+ public function reset_on_attachment_delete( $post_id ) {
+ if ( $this->logo['id'] == $post_id ) {
+ $this->remove_site_logo();
+ }
+ }
+
+ /**
+ * Determine if a site logo is assigned or not.
+ *
+ * @uses Site_Logo::$logo
+ * @return boolean True if there is an active logo, false otherwise
+ */
+ public function has_site_logo() {
+ return ( isset( $this->logo['id'] ) && 0 !== $this->logo['id'] ) ? true : false;
+ }
+
+ /**
+ * Reset the site logo option to zero (empty).
+ *
+ * @uses update_option()
+ */
+ public function remove_site_logo() {
+ update_option(
+ 'site_logo',
+ array(
+ 'id' => (int) 0,
+ 'sizes' => array(),
+ 'url' => '',
+ )
+ );
+ }
+
+ /**
+ * Adds custom classes to the array of body classes.
+ *
+ * @uses Site_Logo::has_site_logo()
+ * @return array Array of <body> classes
+ */
+ public function body_classes( $classes ) {
+ // Add a class if a Site Logo is active
+ if ( $this->has_site_logo() ) {
+ $classes[] = 'has-site-logo';
+ }
+
+ return $classes;
+ }
+
+ /**
+ * Sanitize our header text Customizer setting.
+ *
+ * @param $input
+ * @return mixed 1 if checked, empty string if not checked.
+ */
+ public function sanitize_checkbox( $input ) {
+ return ( 1 == $input ) ? 1 : '';
+ }
+
+ /**
+ * Validate and sanitize a new site logo setting.
+ *
+ * @param $input
+ * @return mixed 1 if checked, empty string if not checked.
+ */
+ public function sanitize_logo_setting( $input ) {
+ $input['id'] = absint( $input['id'] );
+ $input['url'] = esc_url_raw( $input['url'] );
+
+ // If the new setting doesn't point to a valid attachment, just reset the whole thing.
+ if ( false == wp_get_attachment_image_src( $input['id'] ) ) {
+ $input = array(
+ 'id' => (int) 0,
+ 'sizes' => array(),
+ 'url' => '',
+ );
+ }
+
+ return $input;
+ }
+}
+
+/**
+ * Allow themes and plugins to access Site_Logo methods and properties.
+ *
+ * @uses Site_Logo::instance()
+ * @return object Site_Logo
+ */
+function site_logo() {
+ return Site_Logo::instance();
+}
+
+/**
+ * One site logo, please.
+ */
+site_logo();
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/inc/compat.php b/plugins/jetpack/modules/theme-tools/site-logo/inc/compat.php
new file mode 100644
index 00000000..d1098536
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/inc/compat.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Functions for maintaining backwards compatibility with unprefixed template tags from the original Site Logo plugin.
+ * These should never be used in themes; instead, use the template tags in functions.php.
+ * See: https://github.com/Automattic/jetpack/pull/956
+ *
+ * @package Jetpack
+ */
+
+if ( ! function_exists( 'the_site_logo' ) ) :
+ /**
+ * Unprefixed, backwards-compatible function for outputting the site logo.
+ *
+ * @uses jetpack_the_site_logo()
+ */
+ function the_site_logo() {
+ jetpack_the_site_logo();
+ }
+endif;
+
+if ( ! function_exists( 'has_site_logo' ) ) :
+ /**
+ * Unprefixed, backwards-compatible function for determining if a site logo has been set.
+ *
+ * @uses jetpack_has_site_logo()
+ * @return bool True if a site logo is set, false otherwise.
+ */
+ function has_site_logo() {
+ return jetpack_has_site_logo();
+ }
+endif;
+
+if ( ! function_exists( 'get_site_logo' ) ) :
+ /**
+ * Unprefixed, backwards-compatible function for getting either the site logo's image URL or its ID.
+ *
+ * @param string $show Return the site logo URL or ID.
+ * @uses jetpack_get_site_logo()
+ * @return string Site logo ID or URL (the default).
+ */
+ function get_site_logo( $show = 'url' ) {
+ return jetpack_get_site_logo( $show );
+ }
+endif;
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/inc/functions.php b/plugins/jetpack/modules/theme-tools/site-logo/inc/functions.php
new file mode 100644
index 00000000..3b27b32f
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/inc/functions.php
@@ -0,0 +1,176 @@
+<?php
+/**
+ * Functions and template tags for using site logos.
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Retrieve the site logo URL or ID (URL by default). Pass in the string 'id' for ID.
+ *
+ * @uses get_option()
+ * @uses esc_url_raw()
+ * @uses set_url_scheme()
+ * @return mixed The URL or ID of our site logo, false if not set
+ * @since 1.0
+ */
+function jetpack_get_site_logo( $show = 'url' ) {
+ $logo = site_logo()->logo;
+
+ // Return false if no logo is set
+ if ( ! isset( $logo['id'] ) || 0 == $logo['id'] ) {
+ return false;
+ }
+
+ // Return the ID if specified, otherwise return the URL by default
+ if ( 'id' == $show ) {
+ return $logo['id'];
+ } else {
+ return esc_url_raw( set_url_scheme( $logo['url'] ) );
+ }
+}
+
+/**
+ * Retrieve an array of the dimensions of the Site Logo.
+ *
+ * @uses Site_Logo::theme_size()
+ * @uses get_option( 'thumbnail_size_w' )
+ * @uses get_option( 'thumbnail_size_h' )
+ * @uses global $_wp_additional_image_sizes;
+ *
+ * @since 3.6.0
+ *
+ * @return array $dimensions {
+ * An array of dimensions of the Site Logo.
+ *
+ * @type string $width Width of the logo in pixels.
+ * @type string $height Height of the logo in pixels.
+ * }
+ */
+function jetpack_get_site_logo_dimensions() {
+ // Get the image size to use with the logo.
+ $size = site_logo()->theme_size();
+
+ // If the size is the default `thumbnail`, get its dimensions. Otherwise, get them from $_wp_additional_image_sizes
+ if ( empty( $size ) ) {
+ return false;
+ } elseif ( 'thumbnail' == $size ) {
+ $dimensions = array(
+ 'width' => get_option( 'thumbnail_size_w' ),
+ 'height' => get_option( 'thumbnail_size_h' ),
+ );
+ } else {
+ global $_wp_additional_image_sizes;
+
+ if ( ! isset( $_wp_additional_image_sizes[ $size ] ) ) {
+ return false;
+ }
+
+ $dimensions = array(
+ 'width' => $_wp_additional_image_sizes[ $size ]['width'],
+ 'height' => $_wp_additional_image_sizes[ $size ]['height'],
+ );
+ }
+
+ return $dimensions;
+}
+
+/**
+ * Determine if a site logo is assigned or not.
+ *
+ * @uses get_option
+ * @return boolean True if there is an active logo, false otherwise
+ */
+function jetpack_has_site_logo() {
+ return site_logo()->has_site_logo();
+}
+
+/**
+ * Output an <img> tag of the site logo, at the size specified
+ * in the theme's add_theme_support() declaration.
+ *
+ * @uses Site_Logo::logo
+ * @uses Site_Logo::theme_size()
+ * @uses jetpack_has_site_logo()
+ * @uses jetpack_is_customize_preview()
+ * @uses esc_url()
+ * @uses home_url()
+ * @uses esc_attr()
+ * @uses wp_get_attachment_image()
+ * @uses apply_filters()
+ * @since 1.0
+ */
+function jetpack_the_site_logo() {
+ $logo = site_logo()->logo;
+ $logo_id = get_theme_mod( 'custom_logo' ); // Check for WP 4.5 Site Logo
+ $logo_id = $logo_id ? $logo_id : $logo['id']; // Use WP Core logo if present, otherwise use Jetpack's.
+ $size = site_logo()->theme_size();
+ $html = '';
+
+ // If no logo is set, but we're in the Customizer, leave a placeholder (needed for the live preview).
+ if ( ! jetpack_has_site_logo() ) {
+ if ( jetpack_is_customize_preview() ) {
+ $html = sprintf(
+ '<a href="%1$s" class="site-logo-link" style="display:none;"><img class="site-logo" data-size="%2$s" /></a>',
+ esc_url( home_url( '/' ) ),
+ esc_attr( $size )
+ );
+ }
+ }
+
+ // We have a logo. Logo is go.
+ else {
+ $html = sprintf(
+ '<a href="%1$s" class="site-logo-link" rel="home" itemprop="url">%2$s</a>',
+ esc_url( home_url( '/' ) ),
+ wp_get_attachment_image(
+ $logo_id,
+ $size,
+ false,
+ array(
+ 'class' => "site-logo attachment-$size",
+ 'data-size' => $size,
+ 'itemprop' => 'logo',
+ )
+ )
+ );
+ }
+
+ /**
+ * Filter the Site Logo output.
+ *
+ * @module theme-tools
+ *
+ * @since 3.2.0
+ *
+ * @param string $html Site Logo HTML output.
+ * @param array $logo Array of Site Logo details.
+ * @param string $size Size specified in add_theme_support declaration, or 'thumbnail' default.
+ */
+ echo apply_filters( 'jetpack_the_site_logo', $html, $logo, $size );
+}
+
+/**
+ * Whether the site is being previewed in the Customizer.
+ * Duplicate of core function until 4.0 is released.
+ *
+ * @global WP_Customize_Manager $wp_customize Customizer instance.
+ * @return bool True if the site is being previewed in the Customizer, false otherwise.
+ */
+function jetpack_is_customize_preview() {
+ global $wp_customize;
+
+ return is_a( $wp_customize, 'WP_Customize_Manager' ) && $wp_customize->is_preview();
+}
+
+/**
+ * Sanitize the string of classes used for header text.
+ * Limit to A-Z,a-z,0-9,(space),(comma),_,-
+ *
+ * @return string Sanitized string of CSS classes.
+ */
+function jetpack_sanitize_header_text_classes( $classes ) {
+ $classes = preg_replace( '/[^A-Za-z0-9\,\ ._-]/', '', $classes );
+
+ return $classes;
+}
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-control.js b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-control.js
new file mode 100644
index 00000000..c4cce6c2
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-control.js
@@ -0,0 +1,160 @@
+/**
+ * JS for handling the Site Logo Customizer control.
+ */
+( function( wp, $ ) {
+ // nice shortcut
+ var api = wp.customize;
+ /**
+ * The Customizer looks for wp.customizer.controlConstructor[type] functions
+ * where type == the type member of a WP_Customize_Control
+ */
+ api.controlConstructor.site_logo = api.Control.extend( {
+ /**
+ * This method is called when the control is ready to run.
+ * Do all of your setup and event binding here.
+ */
+ ready: function() {
+ // this.container is a jQuery object of your container
+
+ // grab the bits of data from the title for specifying this control
+ var data = this.container.find( '.customize-control-title' ).data();
+
+ // Use specific l10n data for this control where available
+ this.l10n = data.l10n;
+ // Grab mime type
+ this.mime = data.mime;
+
+ // Set up image container and button elements. Cache for re-use.
+ this.$imgContainer = $( '#customize-control-site_logo .current' );
+ this.$btnContainer = $( '#customize-control-site_logo .actions' );
+ this.$img = $( '<img class="site-logo-thumbnail" />' ).prependTo( this.$imgContainer );
+ this.$placeholder = $( '<span>' + this.l10n.placeholder + '</span>' ).prependTo(
+ this.$imgContainer
+ );
+ this.$btnAdd = $(
+ '<button type="button" class="button new">' + this.l10n.upload + '</button>'
+ ).prependTo( this.$btnContainer );
+ this.$btnChange = $(
+ '<button type="button" class="button change">' + this.l10n.change + '</button>'
+ ).prependTo( this.$btnContainer );
+ this.$btnRemove = $(
+ '<button type="button" class="button remove">' + this.l10n.remove + '</button>'
+ ).prependTo( this.$btnContainer );
+
+ // handy shortcut so we don't have to us _.bind every time we add a callback
+ _.bindAll( this, 'removeImg', 'upload', 'render', 'pick' );
+
+ this.$btnAdd.on( 'click', this.upload );
+ this.$btnChange.on( 'click', this.upload );
+ this.$btnRemove.on( 'click', this.removeImg );
+
+ // Call render method whenever setting is changed.
+ this.setting.bind( 'change', this.render );
+ // Do initial rendering.
+ this.render();
+ },
+ /**
+ * Remember that _.bind was used to maintain `this` as the control
+ * object rather than the usual jQuery way of binding to the DOM element.
+ */
+ upload: function( event ) {
+ event.preventDefault();
+
+ if ( ! this.frame ) {
+ this.initFrame();
+ }
+
+ this.frame.open();
+ },
+ /**
+ * Set the media frame so that it can be reused and accessed when needed.
+ */
+ initFrame: function() {
+ this.frame = wp.media( {
+ // The title of the media modal
+ title: this.l10n.choose,
+ // restrict to specified mime type
+ library: {
+ type: this.mime,
+ },
+ // Customize the submit button.
+ button: {
+ // Set the text of the button.
+ text: this.l10n.set,
+ },
+ // Just one, thanks.
+ multiple: false,
+ } );
+
+ // When an image is selected, run a callback.
+ this.frame.on( 'select', this.pick );
+ },
+ /**
+ * Fired when an image is selected in the media modal. Gets the selected
+ * image information, and sets it within the control.
+ */
+ pick: function() {
+ // get the attachment from the modal frame
+ var attachment = this.frame
+ .state()
+ .get( 'selection' )
+ .single();
+ if ( 'image' === attachment.get( 'type' ) ) {
+ // set the setting - the callback will take care of rendering
+ this.setting( this.reduceMembers( attachment.toJSON() ) );
+ }
+ },
+ /**
+ * Reduces the attachment object to just the few desired members.
+ * @param {object} attachment An attachment object provided by the
+ * medial modal.
+ * @return {object} A reduced media object.
+ */
+ reduceMembers: function( attachment ) {
+ var desired = [ 'id', 'sizes', 'url' ],
+ output = {};
+ $.each( desired, function( i, key ) {
+ output[ key ] = attachment[ key ];
+ } );
+ return output;
+ },
+ /**
+ * Called on init and whenever a setting is changed. Shows the thumbnail
+ * when there is one or the upload button when there isn't.
+ */
+ render: function() {
+ var value = this.setting();
+
+ if ( value && value.url ) {
+ this.$placeholder.hide();
+ if ( ! value.sizes || ! value.sizes.medium ) {
+ this.$img.attr( 'src', value.url );
+ } else {
+ this.$img.attr( 'src', value.sizes.medium.url );
+ }
+ this.$img.show();
+ this.$btnRemove.show();
+ this.$btnChange.show();
+ this.$btnAdd.hide();
+ } else {
+ this.$img.hide();
+ this.$placeholder.show();
+ this.$btnRemove.hide();
+ this.$btnChange.hide();
+ this.$btnAdd.show();
+ }
+ },
+ /**
+ * Called when the "Remove Image" link is clicked. Sets thes setting back
+ * to its default state.
+ * @param {object} event jQuery Event object from click event
+ */
+ removeImg: function( event ) {
+ event.preventDefault();
+ this.setting( {
+ url: '',
+ id: 0,
+ } );
+ },
+ } );
+} )( this.wp, jQuery );
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-control.min.js b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-control.min.js
new file mode 100644
index 00000000..171d98e8
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-control.min.js
@@ -0,0 +1 @@
+(function(a,c){var b=a.customize;b.controlConstructor.site_logo=b.Control.extend({ready:function(){var d=this.container.find(".customize-control-title").data();this.l10n=d.l10n;this.mime=d.mime;this.$imgContainer=c("#customize-control-site_logo .current");this.$btnContainer=c("#customize-control-site_logo .actions");this.$img=c('<img class="site-logo-thumbnail" />').prependTo(this.$imgContainer);this.$placeholder=c("<span>"+this.l10n.placeholder+"</span>").prependTo(this.$imgContainer);this.$btnAdd=c('<button type="button" class="button new">'+this.l10n.upload+"</button>").prependTo(this.$btnContainer);this.$btnChange=c('<button type="button" class="button change">'+this.l10n.change+"</button>").prependTo(this.$btnContainer);this.$btnRemove=c('<button type="button" class="button remove">'+this.l10n.remove+"</button>").prependTo(this.$btnContainer);_.bindAll(this,"removeImg","upload","render","pick");this.$btnAdd.on("click",this.upload);this.$btnChange.on("click",this.upload);this.$btnRemove.on("click",this.removeImg);this.setting.bind("change",this.render);this.render()},upload:function(d){d.preventDefault();if(!this.frame){this.initFrame()}this.frame.open()},initFrame:function(){this.frame=a.media({title:this.l10n.choose,library:{type:this.mime},button:{text:this.l10n.set},multiple:false});this.frame.on("select",this.pick)},pick:function(){var d=this.frame.state().get("selection").first().toJSON();d=this.reduceMembers(d);this.setting(d)},reduceMembers:function(f){var e=["id","sizes","url"],d={};c.each(e,function(h,g){d[g]=f[g]});return d},render:function(){var d=this.setting();if(d&&d.url){this.$placeholder.hide();if(!d.sizes||!d.sizes.medium){this.$img.attr("src",d.url)}else{this.$img.attr("src",d.sizes.medium.url)}this.$img.show();this.$btnRemove.show();this.$btnChange.show();this.$btnAdd.hide()}else{this.$img.hide();this.$placeholder.show();this.$btnRemove.hide();this.$btnChange.hide();this.$btnAdd.show()}},removeImg:function(d){d.preventDefault();this.setting({url:"",id:0})}})})(this.wp,jQuery); \ No newline at end of file
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-header-text.js b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-header-text.js
new file mode 100644
index 00000000..82673a7e
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-header-text.js
@@ -0,0 +1,24 @@
+/* global site_logo_header_classes */
+/**
+ * JS for handling the "Display Header Text" setting's realtime preview.
+ */
+( function( $ ) {
+ var api = wp.customize,
+ $classes = site_logo_header_classes;
+
+ api( 'site_logo_header_text', function( value ) {
+ value.bind( function( to ) {
+ if ( true === to ) {
+ $( $classes ).css( {
+ position: 'static',
+ clip: 'auto',
+ } );
+ } else {
+ $( $classes ).css( {
+ position: 'absolute',
+ clip: 'rect(1px 1px 1px 1px)',
+ } );
+ }
+ } );
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-header-text.min.js b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-header-text.min.js
new file mode 100644
index 00000000..39c56ec0
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo-header-text.min.js
@@ -0,0 +1 @@
+!function(t){var i=wp.customize,o=site_logo_header_classes;i("site_logo_header_text",function(i){i.bind(function(i){t(o).css(!0===i?{position:"static",clip:"auto"}:{position:"absolute",clip:"rect(1px 1px 1px 1px)"})})})}(jQuery); \ No newline at end of file
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo.js b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo.js
new file mode 100644
index 00000000..6c959c0c
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo.js
@@ -0,0 +1,46 @@
+/**
+ * JS for handling the Site Logo real-time display in the Customizer preview frame.
+ */
+( function( $ ) {
+ var api = wp.customize,
+ $body,
+ $anchor,
+ $logo,
+ size;
+
+ function cacheSelectors() {
+ $body = $( 'body' );
+ $anchor = $( '.site-logo-link' );
+ $logo = $( '.site-logo' );
+ size = $logo.attr( 'data-size' );
+ }
+
+ api( 'site_logo', function( value ) {
+ value.bind( function( newVal ) {
+ // grab selectors the first time through
+ if ( ! $body ) {
+ cacheSelectors();
+ }
+
+ // Let's update our preview logo.
+ if ( newVal && newVal.url ) {
+ // If the source was smaller than the size required by the theme, give the biggest we've got.
+ if ( ! newVal.sizes[ size ] ) {
+ size = 'full';
+ }
+
+ $logo.attr( {
+ height: newVal.sizes[ size ].height,
+ width: newVal.sizes[ size ].width,
+ src: newVal.sizes[ size ].url,
+ } );
+
+ $anchor.show();
+ $body.addClass( 'has-site-logo' );
+ } else {
+ $anchor.hide();
+ $body.removeClass( 'has-site-logo' );
+ }
+ } );
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo.min.js b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo.min.js
new file mode 100644
index 00000000..a6fe41eb
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/site-logo/js/site-logo.min.js
@@ -0,0 +1 @@
+(function(d){var e=wp.customize,c,f,g,a;e("site_logo",function(e){e.bind(function(b){c||(c=d("body"),f=d(".site-logo-link"),g=d(".site-logo"),a=g.attr("data-size"));b&&b.url?(b.sizes[a]||(a="full"),g.attr({height:b.sizes[a].height,width:b.sizes[a].width,src:b.sizes[a].url}),f.show(),c.addClass("has-site-logo")):(f.hide(),c.removeClass("has-site-logo"))})})})(jQuery); \ No newline at end of file
diff --git a/plugins/jetpack/modules/theme-tools/social-links.php b/plugins/jetpack/modules/theme-tools/social-links.php
new file mode 100644
index 00000000..44b2cbc8
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/social-links.php
@@ -0,0 +1,252 @@
+<?php
+/**
+ * Social Links.
+ *
+ * This feature will only be activated for themes that declare their support.
+ * This can be done by adding code similar to the following during the
+ * 'after_setup_theme' action:
+ *
+ * add_theme_support( 'social-links', array(
+ * 'facebook', 'twitter', 'linkedin', 'tumblr', 'google_plus',
+ * ) );
+ */
+
+function jetpack_theme_supports_social_links() {
+ if ( current_theme_supports( 'social-links' ) && function_exists( 'publicize_init' ) ) {
+ new Social_Links();
+ }
+}
+add_action( 'init', 'jetpack_theme_supports_social_links', 30 );
+
+if ( ! class_exists( 'Social_Links' ) ) {
+
+ class Social_Links {
+
+ /**
+ * The links the user set for each service.
+ *
+ * @var array
+ */
+ private $links;
+
+ /**
+ * A Publicize object.
+ *
+ * @var Publicize
+ */
+ private $publicize;
+
+ /**
+ * An array with all services that are supported by both Publicize and the
+ * currently active theme.
+ *
+ * @var array
+ */
+ private $services = array();
+
+ /**
+ * An array of the services the theme supports
+ *
+ * @var array
+ */
+ private $theme_supported_services = array();
+
+ /**
+ * Constructor.
+ */
+ public function __construct() {
+ $theme_support = get_theme_support( 'social-links' );
+
+ /*
+ An array of named arguments must be passed as the second parameter
+ * of add_theme_support().
+ */
+ if ( empty( $theme_support[0] ) ) {
+ return;
+ }
+
+ $this->theme_supported_services = $theme_support[0];
+ $this->links = Jetpack_Options::get_option( 'social_links', array() );
+
+ $this->admin_setup();
+
+ add_filter( 'jetpack_has_social_links', array( $this, 'has_social_links' ) );
+ add_filter( 'jetpack_get_social_links', array( $this, 'get_social_links' ) );
+
+ foreach ( $theme_support[0] as $service ) {
+ add_filter( "pre_option_jetpack-$service", array( $this, 'get_social_link_filter' ) ); // get_option( 'jetpack-service' );
+ add_filter( "theme_mod_jetpack-$service", array( $this, 'get_social_link_filter' ) ); // get_theme_mod( 'jetpack-service' );
+ }
+ }
+
+ public function admin_setup() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ if ( ! is_admin() && ! $this->is_customize_preview() ) {
+ return;
+ }
+
+ $this->publicize = publicize_init();
+ $publicize_services = $this->publicize->get_services( 'connected' );
+ $this->services = array_intersect( array_keys( $publicize_services ), $this->theme_supported_services );
+
+ add_action( 'publicize_connected', array( $this, 'check_links' ), 20 );
+ add_action( 'publicize_disconnected', array( $this, 'check_links' ), 20 );
+ add_action( 'customize_register', array( $this, 'customize_register' ) );
+ add_filter( 'sanitize_option_jetpack_options', array( $this, 'sanitize_link' ) );
+ }
+
+ /**
+ * Compares the currently saved links with the connected services and removes
+ * links from services that are no longer connected.
+ *
+ * @return void
+ */
+ public function check_links() {
+ $active_links = array_intersect_key( $this->links, array_flip( $this->services ) );
+
+ if ( $active_links !== $this->links ) {
+ $this->links = $active_links;
+ Jetpack_Options::update_option( 'social_links', $active_links );
+ }
+ }
+
+ /**
+ * Add social link dropdown to the Customizer.
+ *
+ * @param WP_Customize_Manager $wp_customize Theme Customizer object.
+ */
+ public function customize_register( $wp_customize ) {
+ $wp_customize->add_section(
+ 'jetpack_social_links',
+ array(
+ 'title' => esc_html__( 'Connect', 'jetpack' ),
+ 'priority' => 35,
+ )
+ );
+
+ foreach ( array_keys( $this->publicize->get_services( 'all' ) ) as $service ) {
+ $choices = $this->get_customize_select( $service );
+
+ if ( empty( $choices ) ) {
+ continue;
+ }
+
+ $wp_customize->add_setting(
+ "jetpack_options[social_links][$service]",
+ array(
+ 'type' => 'option',
+ 'default' => '',
+ )
+ );
+
+ $wp_customize->add_control(
+ "jetpack-$service",
+ array(
+ 'label' => $this->publicize->get_service_label( $service ),
+ 'section' => 'jetpack_social_links',
+ 'settings' => "jetpack_options[social_links][$service]",
+ 'type' => 'select',
+ 'choices' => $choices,
+ )
+ );
+ }
+ }
+
+ /**
+ * Sanitizes social links.
+ *
+ * @param array $option The incoming values to be sanitized.
+ * @returns array
+ */
+ public function sanitize_link( $option ) {
+ foreach ( $this->services as $service ) {
+ if ( ! empty( $option['social_links'][ $service ] ) ) {
+ $option['social_links'][ $service ] = esc_url_raw( $option['social_links'][ $service ] );
+ } else {
+ unset( $option['social_links'][ $service ] );
+ }
+ }
+
+ return $option;
+ }
+
+ /**
+ * Returns whether there are any social links set.
+ *
+ * @returns bool
+ */
+ public function has_social_links() {
+ return ! empty( $this->links );
+ }
+
+ /**
+ * Return available social links.
+ *
+ * @returns array
+ */
+ public function get_social_links() {
+ return $this->links;
+ }
+
+ /**
+ * Short-circuits get_option and get_theme_mod calls.
+ *
+ * @param string $link The incoming value to be replaced.
+ * @returns string $link The social link that we've got.
+ */
+ public function get_social_link_filter( $link ) {
+ if ( preg_match( '/_jetpack-(.+)$/i', current_filter(), $matches ) && ! empty( $this->links[ $matches[1] ] ) ) {
+ return $this->links[ $matches[1] ];
+ }
+
+ return $link;
+ }
+
+ /**
+ * Puts together an array of choices for a specific service.
+ *
+ * @param string $service The social service.
+ * @return array An associative array with profile links and display names.
+ */
+ private function get_customize_select( $service ) {
+ $choices = array(
+ '' => __( '&mdash; Select &mdash;', 'jetpack' ),
+ );
+
+ if ( isset( $this->links[ $service ] ) ) {
+ $choices[ $this->links[ $service ] ] = $this->links[ $service ];
+ }
+
+ $connected_services = $this->publicize->get_services( 'connected' );
+ if ( isset( $connected_services[ $service ] ) ) {
+ foreach ( $connected_services[ $service ] as $c ) {
+ $profile_link = $this->publicize->get_profile_link( $service, $c );
+
+ if ( false === $profile_link ) {
+ continue;
+ }
+
+ $choices[ $profile_link ] = $this->publicize->get_display_name( $service, $c );
+ }
+ }
+
+ if ( 1 === count( $choices ) ) {
+ return array();
+ }
+
+ return $choices;
+ }
+
+ /**
+ * Back-compat function for versions prior to 4.0.
+ */
+ private function is_customize_preview() {
+ global $wp_customize;
+ return is_a( $wp_customize, 'WP_Customize_Manager' ) && $wp_customize->is_preview();
+ }
+ }
+
+} // end if ( ! class_exists( 'Social_Links' )
diff --git a/plugins/jetpack/modules/theme-tools/social-menu.php b/plugins/jetpack/modules/theme-tools/social-menu.php
new file mode 100644
index 00000000..16c66b6a
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/social-menu.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * Social Menu.
+ *
+ * This feature will only be activated for themes that declare their support.
+ * This can be done by adding code similar to the following during the
+ * 'after_setup_theme' action:
+ *
+ * add_theme_support( 'jetpack-social-menu' );
+ */
+
+/**
+ * Activate the Social Menu plugin.
+ *
+ * @uses current_theme_supports()
+ */
+function jetpack_social_menu_init() {
+ // Only load our code if our theme declares support
+ if ( ! current_theme_supports( 'jetpack-social-menu' ) ) {
+ return;
+ }
+
+ /*
+ * Social Menu description.
+ *
+ * Rename the social menu description.
+ *
+ * @module theme-tools
+ *
+ * @since 3.9.0
+ *
+ * @param string $social_menu_description Social Menu description
+ */
+ $social_menu_description = apply_filters( 'jetpack_social_menu_description', __( 'Social Menu', 'jetpack' ) );
+
+ // Register a new menu location
+ register_nav_menus(
+ array(
+ 'jetpack-social-menu' => esc_html( $social_menu_description ),
+ )
+ );
+
+ // Enqueue CSS
+ add_action( 'wp_enqueue_scripts', 'jetpack_social_menu_style' );
+
+ // Load SVG icons related functions and filters
+ if ( 'svg' === jetpack_social_menu_get_type() ) {
+ require dirname( __FILE__ ) . '/social-menu/icon-functions.php';
+ }
+}
+add_action( 'after_setup_theme', 'jetpack_social_menu_init', 99 );
+
+/**
+ * Return the type of menu the theme is using.
+ *
+ * @uses get_theme_support()
+ * @return null|string $menu_type
+ */
+function jetpack_social_menu_get_type() {
+ $options = get_theme_support( 'jetpack-social-menu' );
+
+ if ( empty( $options ) ) {
+ $menu_type = null;
+ } else {
+ $menu_type = ( in_array( $options[0], array( 'genericons', 'svg' ) ) ) ? $options[0] : 'genericons';
+ }
+
+ return $menu_type;
+}
+
+/**
+ * Function to enqueue the CSS.
+ */
+function jetpack_social_menu_style() {
+ $menu_type = jetpack_social_menu_get_type();
+
+ if ( ! $menu_type ) {
+ return;
+ }
+
+ $deps = ( 'genericons' === $menu_type ) ? array( 'genericons' ) : null;
+
+ if ( has_nav_menu( 'jetpack-social-menu' ) ) {
+ wp_enqueue_style( 'jetpack-social-menu', plugins_url( 'social-menu/social-menu.css', __FILE__ ), $deps, '1.0' );
+ }
+}
+
+/**
+ * Create the function for the menu.
+ */
+function jetpack_social_menu() {
+ if ( has_nav_menu( 'jetpack-social-menu' ) ) :
+ $menu_type = jetpack_social_menu_get_type();
+ $link_after = '</span>';
+
+ if ( 'svg' === $menu_type ) {
+ $link_after .= jetpack_social_menu_get_svg( array( 'icon' => 'chain' ) );
+ } ?>
+ <nav class="jetpack-social-navigation jetpack-social-navigation-<?php echo esc_attr( $menu_type ); ?>" role="navigation" aria-label="<?php esc_html_e( 'Social Links Menu', 'jetpack' ); ?>">
+ <?php
+ wp_nav_menu(
+ array(
+ 'theme_location' => 'jetpack-social-menu',
+ 'link_before' => '<span class="screen-reader-text">',
+ 'link_after' => $link_after,
+ 'depth' => 1,
+ )
+ );
+ ?>
+ </nav><!-- .jetpack-social-navigation -->
+ <?php
+ endif;
+}
diff --git a/plugins/jetpack/modules/theme-tools/social-menu/icon-functions.php b/plugins/jetpack/modules/theme-tools/social-menu/icon-functions.php
new file mode 100644
index 00000000..9a8a1dd6
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/social-menu/icon-functions.php
@@ -0,0 +1,177 @@
+<?php
+/**
+ * SVG icons related functions and filters
+ *
+ * @package Jetpack
+ */
+
+if ( ! function_exists( 'jetpack_social_menu_include_svg_icons' ) ) :
+ /**
+ * Add SVG definitions to the footer.
+ */
+ function jetpack_social_menu_include_svg_icons() {
+ // Define SVG sprite file.
+ $svg_icons = dirname( __FILE__ ) . '/social-menu.svg';
+
+ // If it exists, include it.
+ if ( file_exists( $svg_icons ) ) {
+ require_once $svg_icons;
+ }
+ }
+ add_action( 'wp_footer', 'jetpack_social_menu_include_svg_icons', 9999 );
+endif;
+
+if ( ! function_exists( 'jetpack_social_menu_get_svg' ) ) :
+ /**
+ * Return SVG markup.
+ *
+ * @param array $args {
+ * Parameters needed to display an SVG.
+ *
+ * @type string $icon Required SVG icon filename.
+ * }
+ * @return string SVG markup.
+ */
+ function jetpack_social_menu_get_svg( $args = array() ) {
+ // Make sure $args are an array.
+ if ( empty( $args ) ) {
+ return esc_html__( 'Please define default parameters in the form of an array.', 'jetpack' );
+ }
+
+ // Define an icon.
+ if ( false === array_key_exists( 'icon', $args ) ) {
+ return esc_html__( 'Please define an SVG icon filename.', 'jetpack' );
+ }
+
+ // Set defaults.
+ $defaults = array(
+ 'icon' => '',
+ 'fallback' => false,
+ );
+
+ // Parse args.
+ $args = wp_parse_args( $args, $defaults );
+
+ // Set aria hidden.
+ $aria_hidden = ' aria-hidden="true"';
+
+ // Begin SVG markup.
+ $svg = '<svg class="icon icon-' . esc_attr( $args['icon'] ) . '"' . $aria_hidden . ' role="img">';
+
+ /*
+ * Display the icon.
+ *
+ * The whitespace around `<use>` is intentional - it is a work around to a keyboard navigation bug in Safari 10.
+ *
+ * See https://core.trac.wordpress.org/ticket/38387.
+ */
+ $svg .= ' <use href="#icon-' . esc_html( $args['icon'] ) . '" xlink:href="#icon-' . esc_html( $args['icon'] ) . '"></use> ';
+
+ // Add some markup to use as a fallback for browsers that do not support SVGs.
+ if ( $args['fallback'] ) {
+ $svg .= '<span class="svg-fallback icon-' . esc_attr( $args['icon'] ) . '"></span>';
+ }
+
+ $svg .= '</svg>';
+
+ return $svg;
+ }
+endif;
+
+if ( ! function_exists( 'jetpack_social_menu_nav_menu_social_icons' ) ) :
+ /**
+ * Display SVG icons in social links menu.
+ *
+ * @param string $item_output The menu item output.
+ * @param WP_Post $item Menu item object.
+ * @param int $depth Depth of the menu.
+ * @param array $args wp_nav_menu() arguments.
+ * @return string $item_output The menu item output with social icon.
+ */
+ function jetpack_social_menu_nav_menu_social_icons( $item_output, $item, $depth, $args ) {
+ // Get supported social icons.
+ $social_icons = jetpack_social_menu_social_links_icons();
+
+ // Change SVG icon inside social links menu if there is supported URL.
+ if ( 'jetpack-social-menu' === $args->theme_location ) {
+ foreach ( $social_icons as $attr => $value ) {
+ if ( false !== strpos( $item_output, $attr ) ) {
+ $item_output = str_replace( $args->link_after, '</span>' . jetpack_social_menu_get_svg( array( 'icon' => esc_attr( $value ) ) ), $item_output );
+ }
+ }
+ }
+
+ return $item_output;
+ }
+ add_filter( 'walker_nav_menu_start_el', 'jetpack_social_menu_nav_menu_social_icons', 10, 4 );
+endif;
+
+if ( ! function_exists( 'jetpack_social_menu_social_links_icons' ) ) :
+ /**
+ * Returns an array of supported social links (URL and icon name).
+ *
+ * @return array $social_links_icons
+ */
+ function jetpack_social_menu_social_links_icons() {
+ // Supported social links icons.
+ $social_links_icons = array(
+ '500px.com' => '500px',
+ 'amazon.cn' => 'amazon',
+ 'amazon.in' => 'amazon',
+ 'amazon.fr' => 'amazon',
+ 'amazon.de' => 'amazon',
+ 'amazon.it' => 'amazon',
+ 'amazon.nl' => 'amazon',
+ 'amazon.es' => 'amazon',
+ 'amazon.co' => 'amazon',
+ 'amazon.ca' => 'amazon',
+ 'amazon.com' => 'amazon',
+ 'apple.com' => 'apple',
+ 'itunes.com' => 'apple',
+ 'bandcamp.com' => 'bandcamp',
+ 'behance.net' => 'behance',
+ 'codepen.io' => 'codepen',
+ 'deviantart.com' => 'deviantart',
+ 'discord.gg' => 'discord',
+ 'discordapp.com' => 'discord',
+ 'digg.com' => 'digg',
+ 'dribbble.com' => 'dribbble',
+ 'dropbox.com' => 'dropbox',
+ 'etsy.com' => 'etsy',
+ 'facebook.com' => 'facebook',
+ '/feed/' => 'feed',
+ 'flickr.com' => 'flickr',
+ 'foursquare.com' => 'foursquare',
+ 'goodreads.com' => 'goodreads',
+ 'google.com' => 'google',
+ 'github.com' => 'github',
+ 'instagram.com' => 'instagram',
+ 'linkedin.com' => 'linkedin',
+ 'mailto:' => 'mail',
+ 'meetup.com' => 'meetup',
+ 'medium.com' => 'medium',
+ 'pinterest.' => 'pinterest',
+ 'getpocket.com' => 'pocket',
+ 'reddit.com' => 'reddit',
+ 'skype.com' => 'skype',
+ 'skype:' => 'skype',
+ 'slideshare.net' => 'slideshare',
+ 'snapchat.com' => 'snapchat',
+ 'soundcloud.com' => 'soundcloud',
+ 'spotify.com' => 'spotify',
+ 'stackoverflow.com' => 'stackoverflow',
+ 'stumbleupon.com' => 'stumbleupon',
+ 'tumblr.com' => 'tumblr',
+ 'twitch.tv' => 'twitch',
+ 'twitter.com' => 'twitter',
+ 'vimeo.com' => 'vimeo',
+ 'vk.com' => 'vk',
+ 'wordpress.org' => 'wordpress',
+ 'wordpress.com' => 'wordpress',
+ 'yelp.com' => 'yelp',
+ 'youtube.com' => 'youtube',
+ );
+
+ return $social_links_icons;
+ }
+endif;
diff --git a/plugins/jetpack/modules/theme-tools/social-menu/social-menu.css b/plugins/jetpack/modules/theme-tools/social-menu/social-menu.css
new file mode 100644
index 00000000..57a3d7c1
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/social-menu/social-menu.css
@@ -0,0 +1,197 @@
+/*--------------------------------------------------------------
+Global
+--------------------------------------------------------------*/
+.jetpack-social-navigation ul {
+ display: block;
+ margin: 0 0 1.5em;
+ padding: 0;
+}
+
+.jetpack-social-navigation li {
+ display: inline-block;
+ margin: 0;
+ line-height: 1;
+}
+
+.jetpack-social-navigation a {
+ border: 0;
+ height: 1em;
+ text-decoration: none;
+ width: 1em;
+}
+
+/*--------------------------------------------------------------
+SVG
+--------------------------------------------------------------*/
+.jetpack-social-navigation-svg .icon {
+ color: inherit;
+ fill: currentColor;
+ height: 1em;
+ vertical-align: middle;
+ width: 1em;
+}
+
+/*--------------------------------------------------------------
+Genericons
+--------------------------------------------------------------*/
+.jetpack-social-navigation-genericons a:before {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ display: inline-block;
+ font-family: Genericons;
+ font-size: 1em;
+ font-style: normal;
+ font-weight: normal;
+ height: 1em;
+ line-height: 1;
+ speak: none;
+ text-decoration: inherit;
+ vertical-align: top;
+ width: 1em;
+}
+
+/* Default */
+.jetpack-social-navigation-genericons a:before {
+ content: "\f415";
+}
+
+/* CodePen */
+.jetpack-social-navigation-genericons a[href*="codepen.io"]:before {
+ content: "\f216";
+}
+
+/* Digg */
+.jetpack-social-navigation-genericons a[href*="digg.com"]:before {
+ content: "\f221";
+}
+
+/* Dribbble */
+.jetpack-social-navigation-genericons a[href*="dribbble.com"]:before {
+ content: "\f201";
+}
+
+/* Dropbox */
+.jetpack-social-navigation-genericons a[href*="dropbox.com"]:before {
+ content: "\f225";
+}
+
+/* Email */
+.jetpack-social-navigation-genericons a[href*="mailto:"]:before {
+ content: "\f410";
+}
+
+/* Facebook */
+.jetpack-social-navigation-genericons a[href*="facebook.com"]:before {
+ content: "\f203";
+}
+
+/* Flickr */
+.jetpack-social-navigation-genericons a[href*="flickr.com"]:before {
+ content: "\f211";
+}
+
+/* Foursquare */
+.jetpack-social-navigation-genericons a[href*="foursquare.com"]:before {
+ content: "\f226";
+}
+
+/* GitHub */
+.jetpack-social-navigation-genericons a[href*="github.com"]:before {
+ content: "\f200";
+}
+
+/* Google Plus */
+.jetpack-social-navigation-genericons a[href*="plus.google.com"]:before {
+ content: "\f206";
+}
+
+/* Instagram */
+.jetpack-social-navigation-genericons a[href*="instagram.com"]:before {
+ content: "\f215";
+}
+
+/* LinkedIn */
+.jetpack-social-navigation-genericons a[href*="linkedin.com"]:before {
+ content: "\f208";
+}
+
+/* Path */
+.jetpack-social-navigation-genericons a[href*="path.com"]:before {
+ content: "\f219";
+}
+
+/* Pinterest */
+.jetpack-social-navigation-genericons a[href*="pinterest."]:before {
+ content: "\f210";
+}
+
+/* Pocket */
+.jetpack-social-navigation-genericons a[href*="getpocket.com"]:before {
+ content: "\f224";
+}
+
+/* Polldaddy */
+.jetpack-social-navigation-genericons a[href*="polldaddy.com"]:before {
+ content: "\f217";
+}
+
+/* Reddit */
+.jetpack-social-navigation-genericons a[href*="reddit.com"]:before {
+ content: "\f222";
+}
+
+/* RSS Feed */
+.jetpack-social-navigation-genericons a[href$="/feed/"]:before {
+ content: "\f413";
+}
+
+/* Skype */
+.jetpack-social-navigation-genericons a[href*="skype:"]:before {
+ content: "\f220";
+}
+
+/* Spotify */
+.jetpack-social-navigation-genericons a[href*="spotify.com"]:before {
+ content: "\f515";
+}
+
+/* StumbleUpon */
+.jetpack-social-navigation-genericons a[href*="stumbleupon.com"]:before {
+ content: "\f223";
+}
+
+/* Tumblr */
+.jetpack-social-navigation-genericons a[href*="tumblr.com"]:before {
+ content: "\f214";
+}
+
+/* Twitch */
+.jetpack-social-navigation-genericons a[href*="twitch.tv"]:before {
+ content: "\f516";
+}
+
+/* Twitter */
+.jetpack-social-navigation-genericons a[href*="twitter.com"]:before {
+ content: "\f202";
+}
+
+/* Vimeo */
+.jetpack-social-navigation-genericons a[href*="vimeo.com"]:before {
+ content: "\f212";
+}
+
+/* Vine */
+.jetpack-social-navigation-genericons a[href*="vine.co"]:before {
+ content: "\f517";
+}
+
+/* WordPress */
+.jetpack-social-navigation-genericons a[href*="wordpress.com"]:before,
+.jetpack-social-navigation-genericons a[href*="wordpress.org"]:before {
+ content: "\f205";
+}
+
+/* YouTube */
+.jetpack-social-navigation-genericons a[href*="youtube.com"]:before {
+ content: "\f213";
+}
diff --git a/plugins/jetpack/modules/theme-tools/social-menu/social-menu.svg b/plugins/jetpack/modules/theme-tools/social-menu/social-menu.svg
new file mode 100644
index 00000000..896d255f
--- /dev/null
+++ b/plugins/jetpack/modules/theme-tools/social-menu/social-menu.svg
@@ -0,0 +1,137 @@
+<svg style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<defs>
+<symbol id="icon-500px" viewBox="0 0 24 24">
+<path d="M6.94026,15.1412c.00437.01213.108.29862.168.44064a6.55008,6.55008,0,1,0,6.03191-9.09557,6.68654,6.68654,0,0,0-2.58357.51467A8.53914,8.53914,0,0,0,8.21268,8.61344L8.209,8.61725V3.22948l9.0504-.00008c.32934-.0036.32934-.46353.32934-.61466s0-.61091-.33035-.61467L7.47248,2a.43.43,0,0,0-.43131.42692v7.58355c0,.24466.30476.42131.58793.4819.553.11812.68074-.05864.81617-.2457l.018-.02481A10.52673,10.52673,0,0,1,9.32258,9.258a5.35268,5.35268,0,1,1,7.58985,7.54976,5.417,5.417,0,0,1-3.80867,1.56365,5.17483,5.17483,0,0,1-2.69822-.74478l.00342-4.61111a2.79372,2.79372,0,0,1,.71372-1.78792,2.61611,2.61611,0,0,1,1.98282-.89477,2.75683,2.75683,0,0,1,1.95525.79477,2.66867,2.66867,0,0,1,.79656,1.909,2.724,2.724,0,0,1-2.75849,2.748,4.94651,4.94651,0,0,1-.86254-.13719c-.31234-.093-.44519.34058-.48892.48349-.16811.54966.08453.65862.13687.67489a3.75751,3.75751,0,0,0,1.25234.18375,3.94634,3.94634,0,1,0-2.82444-6.742,3.67478,3.67478,0,0,0-1.13028,2.584l-.00041.02323c-.0035.11667-.00579,2.881-.00644,3.78811l-.00407-.00451a6.18521,6.18521,0,0,1-1.0851-1.86092c-.10544-.27856-.34358-.22925-.66857-.12917-.14192.04372-.57386.17677-.47833.489Zm4.65165-1.08338a.51346.51346,0,0,0,.19513.31818l.02276.022a.52945.52945,0,0,0,.3517.18416.24242.24242,0,0,0,.16577-.0611c.05473-.05082.67382-.67812.73287-.738l.69041.68819a.28978.28978,0,0,0,.21437.11032.53239.53239,0,0,0,.35708-.19486c.29792-.30419.14885-.46821.07676-.54751l-.69954-.69975.72952-.73469c.16-.17311.01874-.35708-.12218-.498-.20461-.20461-.402-.25742-.52855-.14083l-.7254.72665-.73354-.73375a.20128.20128,0,0,0-.14179-.05695.54135.54135,0,0,0-.34379.19648c-.22561.22555-.274.38149-.15656.5059l.73374.7315-.72942.73072A.26589.26589,0,0,0,11.59191,14.05782Zm1.59866-9.915A8.86081,8.86081,0,0,0,9.854,4.776a.26169.26169,0,0,0-.16938.22759.92978.92978,0,0,0,.08619.42094c.05682.14524.20779.531.50006.41955a8.40969,8.40969,0,0,1,2.91968-.55484,7.87875,7.87875,0,0,1,3.086.62286,8.61817,8.61817,0,0,1,2.30562,1.49315.2781.2781,0,0,0,.18318.07586c.15529,0,.30425-.15253.43167-.29551.21268-.23861.35873-.4369.1492-.63538a8.50425,8.50425,0,0,0-2.62312-1.694A9.0177,9.0177,0,0,0,13.19058,4.14283ZM19.50945,18.6236h0a.93171.93171,0,0,0-.36642-.25406.26589.26589,0,0,0-.27613.06613l-.06943.06929A7.90606,7.90606,0,0,1,7.60639,18.505a7.57284,7.57284,0,0,1-1.696-2.51537,8.58715,8.58715,0,0,1-.5147-1.77754l-.00871-.04864c-.04939-.25873-.28755-.27684-.62981-.22448-.14234.02178-.5755.088-.53426.39969l.001.00712a9.08807,9.08807,0,0,0,15.406,4.99094c.00193-.00192.04753-.04718.0725-.07436C19.79425,19.16234,19.87422,18.98728,19.50945,18.6236Z"/>
+</symbol>
+<symbol id="icon-amazon" viewBox="0 0 24 24">
+<path d="M13.582,8.182C11.934,8.367,9.78,8.49,8.238,9.166c-1.781,0.769-3.03,2.337-3.03,4.644 c0,2.953,1.86,4.429,4.253,4.429c2.02,0,3.125-0.477,4.685-2.065c0.516,0.747,0.685,1.109,1.629,1.894 c0.212,0.114,0.483,0.103,0.672-0.066l0.006,0.006c0.567-0.505,1.599-1.401,2.18-1.888c0.231-0.188,0.19-0.496,0.009-0.754 c-0.52-0.718-1.072-1.303-1.072-2.634V8.305c0-1.876,0.133-3.599-1.249-4.891C15.23,2.369,13.422,2,12.04,2 C9.336,2,6.318,3.01,5.686,6.351C5.618,6.706,5.877,6.893,6.109,6.945l2.754,0.298C9.121,7.23,9.308,6.977,9.357,6.72 c0.236-1.151,1.2-1.706,2.284-1.706c0.584,0,1.249,0.215,1.595,0.738c0.398,0.584,0.346,1.384,0.346,2.061V8.182z M13.049,14.088 c-0.451,0.8-1.169,1.291-1.967,1.291c-1.09,0-1.728-0.83-1.728-2.061c0-2.42,2.171-2.86,4.227-2.86v0.615 C13.582,12.181,13.608,13.104,13.049,14.088z M20.683,19.339C18.329,21.076,14.917,22,11.979,22c-4.118,0-7.826-1.522-10.632-4.057 c-0.22-0.199-0.024-0.471,0.241-0.317c3.027,1.762,6.771,2.823,10.639,2.823c2.608,0,5.476-0.541,8.115-1.66 C20.739,18.62,21.072,19.051,20.683,19.339z M21.336,21.043c-0.194,0.163-0.379,0.076-0.293-0.139 c0.284-0.71,0.92-2.298,0.619-2.684c-0.301-0.386-1.99-0.183-2.749-0.092c-0.23,0.027-0.266-0.173-0.059-0.319 c1.348-0.946,3.555-0.673,3.811-0.356C22.925,17.773,22.599,19.986,21.336,21.043z"/>
+</symbol>
+<symbol id="icon-apple" viewBox="0 0 24 24">
+<path d="M20.07,17.586a10.874,10.874,0,0,1-1.075,1.933,9.822,9.822,0,0,1-1.385,1.674,2.687,2.687,0,0,1-1.78.784,4.462,4.462,0,0,1-1.644-.393,4.718,4.718,0,0,0-1.77-.391,4.878,4.878,0,0,0-1.82.391A4.9,4.9,0,0,1,9.021,22a2.53,2.53,0,0,1-1.82-.8A10.314,10.314,0,0,1,5.752,19.46,11.987,11.987,0,0,1,4.22,16.417a11.143,11.143,0,0,1-.643-3.627,6.623,6.623,0,0,1,.87-3.465A5.1,5.1,0,0,1,6.268,7.483a4.9,4.9,0,0,1,2.463-.695,5.8,5.8,0,0,1,1.9.443,6.123,6.123,0,0,0,1.511.444,9.04,9.04,0,0,0,1.675-.523,5.537,5.537,0,0,1,2.277-.4,4.835,4.835,0,0,1,3.788,1.994,4.213,4.213,0,0,0-2.235,3.827,4.222,4.222,0,0,0,1.386,3.181,4.556,4.556,0,0,0,1.385.909q-.167.483-.353.927ZM16.211,2.4a4.267,4.267,0,0,1-1.094,2.8,3.726,3.726,0,0,1-3.1,1.528A3.114,3.114,0,0,1,12,6.347a4.384,4.384,0,0,1,1.16-2.828,4.467,4.467,0,0,1,1.414-1.061A4.215,4.215,0,0,1,16.19,2a3.633,3.633,0,0,1,.021.4Z"/>
+</symbol>
+<symbol id="icon-bandcamp" viewBox="0 0 24 24">
+<path d="M15.27 17.289 3 17.289 8.73 6.711 21 6.711 15.27 17.289"/>
+</symbol>
+<symbol id="icon-behance" viewBox="0 0 24 24">
+<path d="M7.799,5.698c0.589,0,1.12,0.051,1.606,0.156c0.482,0.102,0.894,0.273,1.241,0.507c0.344,0.235,0.612,0.546,0.804,0.938 c0.188,0.387,0.281,0.871,0.281,1.443c0,0.619-0.141,1.137-0.421,1.551c-0.284,0.413-0.7,0.751-1.255,1.014 c0.756,0.218,1.317,0.601,1.689,1.146c0.374,0.549,0.557,1.205,0.557,1.975c0,0.623-0.12,1.161-0.359,1.612 c-0.241,0.457-0.569,0.828-0.973,1.114c-0.408,0.288-0.876,0.5-1.399,0.637C9.052,17.931,8.514,18,7.963,18H2V5.698H7.799 M7.449,10.668c0.481,0,0.878-0.114,1.192-0.345c0.311-0.228,0.463-0.603,0.463-1.119c0-0.286-0.051-0.523-0.152-0.707 C8.848,8.315,8.711,8.171,8.536,8.07C8.362,7.966,8.166,7.894,7.94,7.854c-0.224-0.044-0.457-0.06-0.697-0.06H4.709v2.874H7.449z M7.6,15.905c0.267,0,0.521-0.024,0.759-0.077c0.243-0.053,0.457-0.137,0.637-0.261c0.182-0.12,0.332-0.283,0.441-0.491 C9.547,14.87,9.6,14.602,9.6,14.278c0-0.633-0.18-1.084-0.533-1.357c-0.356-0.27-0.83-0.404-1.413-0.404H4.709v3.388L7.6,15.905z M16.162,15.864c0.367,0.358,0.897,0.538,1.583,0.538c0.493,0,0.92-0.125,1.277-0.374c0.354-0.248,0.571-0.514,0.654-0.79h2.155 c-0.347,1.072-0.872,1.838-1.589,2.299C19.534,18,18.67,18.23,17.662,18.23c-0.701,0-1.332-0.113-1.899-0.337 c-0.567-0.227-1.041-0.544-1.439-0.958c-0.389-0.415-0.689-0.907-0.904-1.484c-0.213-0.574-0.32-1.21-0.32-1.899 c0-0.666,0.11-1.288,0.329-1.863c0.222-0.577,0.529-1.075,0.933-1.492c0.406-0.42,0.885-0.751,1.444-0.994 c0.558-0.241,1.175-0.363,1.857-0.363c0.754,0,1.414,0.145,1.98,0.44c0.563,0.291,1.026,0.686,1.389,1.181 c0.363,0.493,0.622,1.057,0.783,1.69c0.16,0.632,0.217,1.292,0.171,1.983h-6.428C15.557,14.84,15.795,15.506,16.162,15.864 M18.973,11.184c-0.291-0.321-0.783-0.496-1.384-0.496c-0.39,0-0.714,0.066-0.973,0.2c-0.254,0.132-0.461,0.297-0.621,0.491 c-0.157,0.197-0.265,0.405-0.328,0.628c-0.063,0.217-0.101,0.413-0.111,0.587h3.98C19.478,11.969,19.265,11.509,18.973,11.184z M15.057,7.738h4.985V6.524h-4.985L15.057,7.738z"/>
+</symbol>
+<symbol id="icon-chain" viewBox="0 0 24 24">
+<path d="M19.647,16.706a1.134,1.134,0,0,0-.343-.833l-2.549-2.549a1.134,1.134,0,0,0-.833-.343,1.168,1.168,0,0,0-.883.392l.233.226q.2.189.264.264a2.922,2.922,0,0,1,.184.233.986.986,0,0,1,.159.312,1.242,1.242,0,0,1,.043.337,1.172,1.172,0,0,1-1.176,1.176,1.237,1.237,0,0,1-.337-.043,1,1,0,0,1-.312-.159,2.76,2.76,0,0,1-.233-.184q-.073-.068-.264-.264l-.226-.233a1.19,1.19,0,0,0-.4.895,1.134,1.134,0,0,0,.343.833L15.837,19.3a1.13,1.13,0,0,0,.833.331,1.18,1.18,0,0,0,.833-.318l1.8-1.789a1.12,1.12,0,0,0,.343-.821Zm-8.615-8.64a1.134,1.134,0,0,0-.343-.833L8.163,4.7a1.134,1.134,0,0,0-.833-.343,1.184,1.184,0,0,0-.833.331L4.7,6.473a1.12,1.12,0,0,0-.343.821,1.134,1.134,0,0,0,.343.833l2.549,2.549a1.13,1.13,0,0,0,.833.331,1.184,1.184,0,0,0,.883-.38L8.728,10.4q-.2-.189-.264-.264A2.922,2.922,0,0,1,8.28,9.9a.986.986,0,0,1-.159-.312,1.242,1.242,0,0,1-.043-.337A1.172,1.172,0,0,1,9.254,8.079a1.237,1.237,0,0,1,.337.043,1,1,0,0,1,.312.159,2.761,2.761,0,0,1,.233.184q.073.068.264.264l.226.233a1.19,1.19,0,0,0,.4-.895ZM22,16.706a3.343,3.343,0,0,1-1.042,2.488l-1.8,1.789a3.536,3.536,0,0,1-4.988-.025l-2.525-2.537a3.384,3.384,0,0,1-1.017-2.488,3.448,3.448,0,0,1,1.078-2.561l-1.078-1.078a3.434,3.434,0,0,1-2.549,1.078,3.4,3.4,0,0,1-2.5-1.029L3.029,9.794A3.4,3.4,0,0,1,2,7.294,3.343,3.343,0,0,1,3.042,4.806l1.8-1.789A3.384,3.384,0,0,1,7.331,2a3.357,3.357,0,0,1,2.5,1.042l2.525,2.537a3.384,3.384,0,0,1,1.017,2.488,3.448,3.448,0,0,1-1.078,2.561l1.078,1.078a3.551,3.551,0,0,1,5.049-.049l2.549,2.549A3.4,3.4,0,0,1,22,16.706Z"/>
+</symbol>
+<symbol id="icon-codepen" viewBox="0 0 24 24">
+<path d="M22.016,8.84c-0.002-0.013-0.005-0.025-0.007-0.037c-0.005-0.025-0.008-0.048-0.015-0.072 c-0.003-0.015-0.01-0.028-0.013-0.042c-0.008-0.02-0.015-0.04-0.023-0.062c-0.007-0.015-0.013-0.028-0.02-0.042 c-0.008-0.02-0.018-0.037-0.03-0.057c-0.007-0.013-0.017-0.027-0.025-0.038c-0.012-0.018-0.023-0.035-0.035-0.052 c-0.01-0.013-0.02-0.025-0.03-0.037c-0.015-0.017-0.028-0.032-0.043-0.045c-0.01-0.012-0.022-0.023-0.035-0.035 c-0.015-0.015-0.032-0.028-0.048-0.04c-0.012-0.01-0.025-0.02-0.037-0.03c-0.005-0.003-0.01-0.008-0.015-0.012l-9.161-6.096 c-0.289-0.192-0.666-0.192-0.955,0L2.359,8.237C2.354,8.24,2.349,8.245,2.344,8.249L2.306,8.277 c-0.017,0.013-0.033,0.027-0.048,0.04C2.246,8.331,2.234,8.342,2.222,8.352c-0.015,0.015-0.028,0.03-0.042,0.047 c-0.012,0.013-0.022,0.023-0.03,0.037C2.139,8.453,2.125,8.471,2.115,8.488C2.107,8.501,2.099,8.514,2.09,8.526 C2.079,8.548,2.069,8.565,2.06,8.585C2.054,8.6,2.047,8.613,2.04,8.626C2.032,8.648,2.025,8.67,2.019,8.69 c-0.005,0.013-0.01,0.027-0.013,0.042C1.999,8.755,1.995,8.778,1.99,8.803C1.989,8.817,1.985,8.828,1.984,8.84 C1.978,8.879,1.975,8.915,1.975,8.954v6.093c0,0.037,0.003,0.075,0.008,0.112c0.002,0.012,0.005,0.025,0.007,0.038 c0.005,0.023,0.008,0.047,0.015,0.072c0.003,0.015,0.008,0.028,0.013,0.04c0.007,0.022,0.013,0.042,0.022,0.063 c0.007,0.015,0.013,0.028,0.02,0.04c0.008,0.02,0.018,0.038,0.03,0.058c0.007,0.013,0.015,0.027,0.025,0.038 c0.012,0.018,0.023,0.035,0.035,0.052c0.01,0.013,0.02,0.025,0.03,0.037c0.013,0.015,0.028,0.032,0.042,0.045 c0.012,0.012,0.023,0.023,0.035,0.035c0.015,0.013,0.032,0.028,0.048,0.04l0.038,0.03c0.005,0.003,0.01,0.007,0.013,0.01 l9.163,6.095C11.668,21.953,11.833,22,12,22c0.167,0,0.332-0.047,0.478-0.144l9.163-6.095l0.015-0.01 c0.013-0.01,0.027-0.02,0.037-0.03c0.018-0.013,0.035-0.028,0.048-0.04c0.013-0.012,0.025-0.023,0.035-0.035 c0.017-0.015,0.03-0.032,0.043-0.045c0.01-0.013,0.02-0.025,0.03-0.037c0.013-0.018,0.025-0.035,0.035-0.052 c0.008-0.013,0.018-0.027,0.025-0.038c0.012-0.02,0.022-0.038,0.03-0.058c0.007-0.013,0.013-0.027,0.02-0.04 c0.008-0.022,0.015-0.042,0.023-0.063c0.003-0.013,0.01-0.027,0.013-0.04c0.007-0.025,0.01-0.048,0.015-0.072 c0.002-0.013,0.005-0.027,0.007-0.037c0.003-0.042,0.007-0.079,0.007-0.117V8.954C22.025,8.915,22.022,8.879,22.016,8.84z M12.862,4.464l6.751,4.49l-3.016,2.013l-3.735-2.492V4.464z M11.138,4.464v4.009l-3.735,2.494L4.389,8.954L11.138,4.464z M3.699,10.562L5.853,12l-2.155,1.438V10.562z M11.138,19.536l-6.749-4.491l3.015-2.011l3.735,2.492V19.536z M12,14.035L8.953,12 L12,9.966L15.047,12L12,14.035z M12.862,19.536v-4.009l3.735-2.492l3.016,2.011L12.862,19.536z M20.303,13.438L18.147,12 l2.156-1.438L20.303,13.438z"/>
+</symbol>
+<symbol id="icon-deviantart" viewBox="0 0 24 24">
+<path d="M 18.19 5.636 18.19 2 18.188 2 14.553 2 14.19 2.366 12.474 5.636 11.935 6 5.81 6 5.81 10.994 9.177 10.994 9.477 11.357 5.81 18.363 5.81 22 5.811 22 9.447 22 9.81 21.634 11.526 18.364 12.065 18 18.19 18 18.19 13.006 14.823 13.006 14.523 12.641 18.19 5.636z"/>
+</symbol>
+<symbol id="icon-digg" viewBox="0 0 24 24">
+<path d="M4.5,5.4h2.2V16H1V8.5h3.5V5.4L4.5,5.4z M4.5,14.2v-4H3.2v4H4.5z M7.6,8.5V16h2.2V8.5C9.8,8.5,7.6,8.5,7.6,8.5z M7.6,5.4 v2.2h2.2V5.4C9.8,5.4,7.6,5.4,7.6,5.4z M10.7,8.5h5.7v10.1h-5.7v-1.8h3.5V16h-3.5C10.7,16,10.7,8.5,10.7,8.5z M14.2,14.2v-4h-1.3v4 H14.2z M17.3,8.5H23v10.1h-5.7v-1.8h3.5V16h-3.5C17.3,16,17.3,8.5,17.3,8.5z M20.8,14.2v-4h-1.3v4H20.8z"/>
+</symbol>
+<symbol id="icon-discord" viewBox="0 0 24 24">
+<path d="M10.227 9.957c-.559 0-1 .48-1 1.063 0 .585.453 1.066 1 1.066.558 0 1-.48 1-1.066.007-.582-.442-1.063-1-1.063zm3.574 0c-.559 0-.996.48-.996 1.063 0 .585.449 1.066.996 1.066.558 0 1-.48 1-1.066 0-.582-.442-1.063-1-1.063zm0 0 M18.563 1.918H5.438c-1.11 0-2.008.879-2.008 1.973v12.957c0 1.093.898 1.972 2.007 1.972h11.11l-.52-1.773 1.254 1.14 1.184 1.075 2.105 1.82V3.891c0-1.094-.898-1.973-2.008-1.973zM14.78 14.434s-.351-.414-.644-.778c1.281-.355 1.773-1.14 1.773-1.14a5.745 5.745 0 0 1-1.129.566c-.488.2-.96.336-1.418.41a7.07 7.07 0 0 1-2.539-.008 8.133 8.133 0 0 1-1.441-.414 6.219 6.219 0 0 1-.715-.324c-.027-.02-.059-.027-.086-.047a.113.113 0 0 1-.039-.031c-.176-.094-.273-.16-.273-.16s.468.765 1.71 1.129c-.293.363-.656.797-.656.797-2.164-.067-2.984-1.457-2.984-1.457 0-3.086 1.41-5.586 1.41-5.586 1.41-1.036 2.75-1.008 2.75-1.008l.098.113c-1.762.5-2.575 1.258-2.575 1.258s.215-.117.579-.277c1.046-.454 1.878-.579 2.222-.606.059-.008.11-.02.168-.02a8.728 8.728 0 0 1 1.977-.019c.933.106 1.93.375 2.949.922 0 0-.773-.719-2.438-1.219l.137-.152s1.34-.028 2.75 1.008c0 0 1.414 2.5 1.414 5.586 0 0-.836 1.39-3 1.457zm0 0"/>
+</symbol>
+<symbol id="icon-dribbble" viewBox="0 0 24 24">
+<path d="M12,22C6.486,22,2,17.514,2,12S6.486,2,12,2c5.514,0,10,4.486,10,10S17.514,22,12,22z M20.434,13.369 c-0.292-0.092-2.644-0.794-5.32-0.365c1.117,3.07,1.572,5.57,1.659,6.09C18.689,17.798,20.053,15.745,20.434,13.369z M15.336,19.876c-0.127-0.749-0.623-3.361-1.822-6.477c-0.019,0.006-0.038,0.013-0.056,0.019c-4.818,1.679-6.547,5.02-6.701,5.334 c1.448,1.129,3.268,1.803,5.243,1.803C13.183,20.555,14.311,20.313,15.336,19.876z M5.654,17.724 c0.193-0.331,2.538-4.213,6.943-5.637c0.111-0.036,0.224-0.07,0.337-0.102c-0.214-0.485-0.448-0.971-0.692-1.45 c-4.266,1.277-8.405,1.223-8.778,1.216c-0.003,0.087-0.004,0.174-0.004,0.261C3.458,14.207,4.29,16.21,5.654,17.724z M3.639,10.264 c0.382,0.005,3.901,0.02,7.897-1.041c-1.415-2.516-2.942-4.631-3.167-4.94C5.979,5.41,4.193,7.613,3.639,10.264z M9.998,3.709 c0.236,0.316,1.787,2.429,3.187,5c3.037-1.138,4.323-2.867,4.477-3.085C16.154,4.286,14.17,3.471,12,3.471 C11.311,3.471,10.641,3.554,9.998,3.709z M18.612,6.612C18.432,6.855,17,8.69,13.842,9.979c0.199,0.407,0.389,0.821,0.567,1.237 c0.063,0.148,0.124,0.295,0.184,0.441c2.842-0.357,5.666,0.215,5.948,0.275C20.522,9.916,19.801,8.065,18.612,6.612z"/>
+</symbol>
+<symbol id="icon-dropbox" viewBox="0 0 24 24">
+<path d="M12,6.134L6.069,9.797L2,6.54l5.883-3.843L12,6.134z M2,13.054l5.883,3.843L12,13.459L6.069,9.797L2,13.054z M12,13.459 l4.116,3.439L22,13.054l-4.069-3.257L12,13.459z M22,6.54l-5.884-3.843L12,6.134l5.931,3.663L22,6.54z M12.011,14.2l-4.129,3.426 l-1.767-1.153v1.291l5.896,3.539l5.897-3.539v-1.291l-1.769,1.153L12.011,14.2z"/>
+</symbol>
+<symbol id="icon-etsy" viewBox="0 0 24 24">
+<path d="M9.16033,4.038c0-.27174.02717-.43478.48913-.43478h6.22283c1.087,0,1.68478.92391,2.11957,2.663l.35326,1.38587h1.05978C19.59511,3.712,19.75815,2,19.75815,2s-2.663.29891-4.23913.29891h-7.962L3.29076,2.163v1.1413L4.731,3.57609c1.00543.19022,1.25.40761,1.33152,1.33152,0,0,.08152,2.71739.08152,7.20109s-.08152,7.17391-.08152,7.17391c0,.81522-.32609,1.11413-1.33152,1.30435l-1.44022.27174V22l4.2663-.13587h7.11957c1.60326,0,5.32609.13587,5.32609.13587.08152-.97826.625-5.40761.70652-5.89674H19.7038L18.644,18.52174c-.84239,1.90217-2.06522,2.038-3.42391,2.038H11.1712c-1.3587,0-2.01087-.54348-2.01087-1.712V12.65217s3.0163,0,3.99457.08152c.76087.05435,1.22283.27174,1.46739,1.33152l.32609,1.413h1.16848l-.08152-3.55978.163-3.587H15.02989l-.38043,1.57609c-.24457,1.03261-.40761,1.22283-1.46739,1.33152-1.38587.13587-4.02174.1087-4.02174.1087Z"/>
+</symbol>
+<symbol id="icon-facebook" viewBox="0 0 24 24">
+<path d="M20.007,3H3.993C3.445,3,3,3.445,3,3.993v16.013C3,20.555,3.445,21,3.993,21h8.621v-6.971h-2.346v-2.717h2.346V9.31 c0-2.325,1.42-3.591,3.494-3.591c0.993,0,1.847,0.074,2.096,0.107v2.43l-1.438,0.001c-1.128,0-1.346,0.536-1.346,1.323v1.734h2.69 l-0.35,2.717h-2.34V21h4.587C20.555,21,21,20.555,21,20.007V3.993C21,3.445,20.555,3,20.007,3z"/>
+</symbol>
+<symbol id="icon-feed" viewBox="0 0 24 24">
+<path d="M2,8.667V12c5.515,0,10,4.485,10,10h3.333C15.333,14.637,9.363,8.667,2,8.667z M2,2v3.333 c9.19,0,16.667,7.477,16.667,16.667H22C22,10.955,13.045,2,2,2z M4.5,17C3.118,17,2,18.12,2,19.5S3.118,22,4.5,22S7,20.88,7,19.5 S5.882,17,4.5,17z"/>
+</symbol>
+<symbol id="icon-flickr" viewBox="0 0 24 24">
+<path d="M6.5,7c-2.75,0-5,2.25-5,5s2.25,5,5,5s5-2.25,5-5S9.25,7,6.5,7z M17.5,7c-2.75,0-5,2.25-5,5s2.25,5,5,5s5-2.25,5-5 S20.25,7,17.5,7z"/>
+</symbol>
+<symbol id="icon-foursquare" viewBox="0 0 24 24">
+<path d="M17.573,2c0,0-9.197,0-10.668,0S5,3.107,5,3.805s0,16.948,0,16.948c0,0.785,0.422,1.077,0.66,1.172 c0.238,0.097,0.892,0.177,1.285-0.275c0,0,5.035-5.843,5.122-5.93c0.132-0.132,0.132-0.132,0.262-0.132h3.26 c1.368,0,1.588-0.977,1.732-1.552c0.078-0.318,0.692-3.428,1.225-6.122l0.675-3.368C19.56,2.893,19.14,2,17.573,2z M16.495,7.22 c-0.053,0.252-0.372,0.518-0.665,0.518c-0.293,0-4.157,0-4.157,0c-0.467,0-0.802,0.318-0.802,0.787v0.508 c0,0.467,0.337,0.798,0.805,0.798c0,0,3.197,0,3.528,0s0.655,0.362,0.583,0.715c-0.072,0.353-0.407,2.102-0.448,2.295 c-0.04,0.193-0.262,0.523-0.655,0.523c-0.33,0-2.88,0-2.88,0c-0.523,0-0.683,0.068-1.033,0.503 c-0.35,0.437-3.505,4.223-3.505,4.223c-0.032,0.035-0.063,0.027-0.063-0.015V4.852c0-0.298,0.26-0.648,0.648-0.648 c0,0,8.228,0,8.562,0c0.315,0,0.61,0.297,0.528,0.683L16.495,7.22z"/>
+</symbol>
+<symbol id="icon-goodreads" viewBox="0 0 24 24">
+<path d="M17.3,17.5c-0.2,0.8-0.5,1.4-1,1.9c-0.4,0.5-1,0.9-1.7,1.2C13.9,20.9,13.1,21,12,21c-0.6,0-1.3-0.1-1.9-0.2 c-0.6-0.1-1.1-0.4-1.6-0.7c-0.5-0.3-0.9-0.7-1.2-1.2c-0.3-0.5-0.5-1.1-0.5-1.7h1.5c0.1,0.5,0.2,0.9,0.5,1.2 c0.2,0.3,0.5,0.6,0.9,0.8c0.3,0.2,0.7,0.3,1.1,0.4c0.4,0.1,0.8,0.1,1.2,0.1c1.4,0,2.5-0.4,3.1-1.2c0.6-0.8,1-2,1-3.5v-1.7h0 c-0.4,0.8-0.9,1.4-1.6,1.9c-0.7,0.5-1.5,0.7-2.4,0.7c-1,0-1.9-0.2-2.6-0.5C8.7,15,8.1,14.5,7.7,14c-0.5-0.6-0.8-1.3-1-2.1 c-0.2-0.8-0.3-1.6-0.3-2.5c0-0.9,0.1-1.7,0.4-2.5c0.3-0.8,0.6-1.5,1.1-2c0.5-0.6,1.1-1,1.8-1.4C10.3,3.2,11.1,3,12,3 c0.5,0,0.9,0.1,1.3,0.2c0.4,0.1,0.8,0.3,1.1,0.5c0.3,0.2,0.6,0.5,0.9,0.8c0.3,0.3,0.5,0.6,0.6,1h0V3.4h1.5V15 C17.6,15.9,17.5,16.7,17.3,17.5z M13.8,14.1c0.5-0.3,0.9-0.7,1.3-1.1c0.3-0.5,0.6-1,0.8-1.6c0.2-0.6,0.3-1.2,0.3-1.9 c0-0.6-0.1-1.2-0.2-1.9c-0.1-0.6-0.4-1.2-0.7-1.7c-0.3-0.5-0.7-0.9-1.3-1.2c-0.5-0.3-1.1-0.5-1.9-0.5s-1.4,0.2-1.9,0.5 c-0.5,0.3-1,0.7-1.3,1.2C8.5,6.4,8.3,7,8.1,7.6C8,8.2,7.9,8.9,7.9,9.5c0,0.6,0.1,1.3,0.2,1.9C8.3,12,8.6,12.5,8.9,13 c0.3,0.5,0.8,0.8,1.3,1.1c0.5,0.3,1.1,0.4,1.9,0.4C12.7,14.5,13.3,14.4,13.8,14.1z"/>
+</symbol>
+<symbol id="icon-google" viewBox="0 0 24 24">
+<path d="M12.02,10.18v3.72v0.01h5.51c-0.26,1.57-1.67,4.22-5.5,4.22c-3.31,0-6.01-2.75-6.01-6.12s2.7-6.12,6.01-6.12 c1.87,0,3.13,0.8,3.85,1.48l2.84-2.76C16.99,2.99,14.73,2,12.03,2c-5.52,0-10,4.48-10,10s4.48,10,10,10c5.77,0,9.6-4.06,9.6-9.77 c0-0.83-0.11-1.42-0.25-2.05H12.02z"/>
+</symbol>
+<symbol id="icon-github" viewBox="0 0 24 24">
+<path d="M12,2C6.477,2,2,6.477,2,12c0,4.419,2.865,8.166,6.839,9.489c0.5,0.09,0.682-0.218,0.682-0.484 c0-0.236-0.009-0.866-0.014-1.699c-2.782,0.602-3.369-1.34-3.369-1.34c-0.455-1.157-1.11-1.465-1.11-1.465 c-0.909-0.62,0.069-0.608,0.069-0.608c1.004,0.071,1.532,1.03,1.532,1.03c0.891,1.529,2.341,1.089,2.91,0.833
+c0.091-0.647,0.349-1.086,0.635-1.337c-2.22-0.251-4.555-1.111-4.555-4.943c0-1.091,0.39-1.984,1.03-2.682 C6.546,8.54,6.202,7.524,6.746,6.148c0,0,0.84-0.269,2.75,1.025C10.295,6.95,11.15,6.84,12,6.836 c0.85,0.004,1.705,0.114,2.504,0.336c1.909-1.294,2.748-1.025,2.748-1.025c0.546,1.376,0.202,2.394,0.1,2.646 c0.64,0.699,1.026,1.591,1.026,2.682c0,3.841-2.337,4.687-4.565,4.935c0.359,0.307,0.679,0.917,0.679,1.852 c0,1.335-0.012,2.415-0.012,2.741c0,0.269,0.18,0.579,0.688,0.481C19.138,20.161,22,16.416,22,12C22,6.477,17.523,2,12,2z"/>
+</symbol>
+<symbol id="icon-instagram" viewBox="0 0 24 24">
+<path d="M12,4.622c2.403,0,2.688,0.009,3.637,0.052c0.877,0.04,1.354,0.187,1.671,0.31c0.42,0.163,0.72,0.358,1.035,0.673 c0.315,0.315,0.51,0.615,0.673,1.035c0.123,0.317,0.27,0.794,0.31,1.671c0.043,0.949,0.052,1.234,0.052,3.637 s-0.009,2.688-0.052,3.637c-0.04,0.877-0.187,1.354-0.31,1.671c-0.163,0.42-0.358,0.72-0.673,1.035 c-0.315,0.315-0.615,0.51-1.035,0.673c-0.317,0.123-0.794,0.27-1.671,0.31c-0.949,0.043-1.233,0.052-3.637,0.052 s-2.688-0.009-3.637-0.052c-0.877-0.04-1.354-0.187-1.671-0.31c-0.42-0.163-0.72-0.358-1.035-0.673 c-0.315-0.315-0.51-0.615-0.673-1.035c-0.123-0.317-0.27-0.794-0.31-1.671C4.631,14.688,4.622,14.403,4.622,12 s0.009-2.688,0.052-3.637c0.04-0.877,0.187-1.354,0.31-1.671c0.163-0.42,0.358-0.72,0.673-1.035 c0.315-0.315,0.615-0.51,1.035-0.673c0.317-0.123,0.794-0.27,1.671-0.31C9.312,4.631,9.597,4.622,12,4.622 M12,3 C9.556,3,9.249,3.01,8.289,3.054C7.331,3.098,6.677,3.25,6.105,3.472C5.513,3.702,5.011,4.01,4.511,4.511 c-0.5,0.5-0.808,1.002-1.038,1.594C3.25,6.677,3.098,7.331,3.054,8.289C3.01,9.249,3,9.556,3,12c0,2.444,0.01,2.751,0.054,3.711 c0.044,0.958,0.196,1.612,0.418,2.185c0.23,0.592,0.538,1.094,1.038,1.594c0.5,0.5,1.002,0.808,1.594,1.038 c0.572,0.222,1.227,0.375,2.185,0.418C9.249,20.99,9.556,21,12,21s2.751-0.01,3.711-0.054c0.958-0.044,1.612-0.196,2.185-0.418 c0.592-0.23,1.094-0.538,1.594-1.038c0.5-0.5,0.808-1.002,1.038-1.594c0.222-0.572,0.375-1.227,0.418-2.185 C20.99,14.751,21,14.444,21,12s-0.01-2.751-0.054-3.711c-0.044-0.958-0.196-1.612-0.418-2.185c-0.23-0.592-0.538-1.094-1.038-1.594 c-0.5-0.5-1.002-0.808-1.594-1.038c-0.572-0.222-1.227-0.375-2.185-0.418C14.751,3.01,14.444,3,12,3L12,3z M12,7.378 c-2.552,0-4.622,2.069-4.622,4.622S9.448,16.622,12,16.622s4.622-2.069,4.622-4.622S14.552,7.378,12,7.378z M12,15 c-1.657,0-3-1.343-3-3s1.343-3,3-3s3,1.343,3,3S13.657,15,12,15z M16.804,6.116c-0.596,0-1.08,0.484-1.08,1.08 s0.484,1.08,1.08,1.08c0.596,0,1.08-0.484,1.08-1.08S17.401,6.116,16.804,6.116z"/>
+</symbol>
+<symbol id="icon-linkedin" viewBox="0 0 24 24">
+<path d="M19.7,3H4.3C3.582,3,3,3.582,3,4.3v15.4C3,20.418,3.582,21,4.3,21h15.4c0.718,0,1.3-0.582,1.3-1.3V4.3 C21,3.582,20.418,3,19.7,3z M8.339,18.338H5.667v-8.59h2.672V18.338z M7.004,8.574c-0.857,0-1.549-0.694-1.549-1.548 c0-0.855,0.691-1.548,1.549-1.548c0.854,0,1.547,0.694,1.547,1.548C8.551,7.881,7.858,8.574,7.004,8.574z M18.339,18.338h-2.669 v-4.177c0-0.996-0.017-2.278-1.387-2.278c-1.389,0-1.601,1.086-1.601,2.206v4.249h-2.667v-8.59h2.559v1.174h0.037 c0.356-0.675,1.227-1.387,2.526-1.387c2.703,0,3.203,1.779,3.203,4.092V18.338z"/>
+</symbol>
+<symbol id="icon-mail" viewBox="0 0 24 24">
+<path d="M20,4H4C2.895,4,2,4.895,2,6v12c0,1.105,0.895,2,2,2h16c1.105,0,2-0.895,2-2V6C22,4.895,21.105,4,20,4z M20,8.236l-8,4.882 L4,8.236V6h16V8.236z"/>
+</symbol>
+<symbol id="icon-meetup" viewBox="0 0 24 24">
+<path d="M19.24775,14.722a3.57032,3.57032,0,0,1-2.94457,3.52073,3.61886,3.61886,0,0,1-.64652.05634c-.07314-.0008-.10187.02846-.12507.09547A2.38881,2.38881,0,0,1,13.49453,20.094a2.33092,2.33092,0,0,1-1.827-.50716.13635.13635,0,0,0-.19878-.00408,3.191,3.191,0,0,1-2.104.60248,3.26309,3.26309,0,0,1-3.00324-2.71993,2.19076,2.19076,0,0,1-.03512-.30865c-.00156-.08579-.03413-.1189-.11608-.13493a2.86421,2.86421,0,0,1-1.23189-.56111,2.945,2.945,0,0,1-1.166-2.05749,2.97484,2.97484,0,0,1,.87524-2.50774.112.112,0,0,0,.02091-.16107,2.7213,2.7213,0,0,1-.36648-1.48A2.81256,2.81256,0,0,1,6.57673,7.58838a.35764.35764,0,0,0,.28869-.22819,4.2208,4.2208,0,0,1,6.02892-1.90111.25161.25161,0,0,0,.22023.0243,3.65608,3.65608,0,0,1,3.76031.90678A3.57244,3.57244,0,0,1,17.95918,8.626a2.97339,2.97339,0,0,1,.01829.57356.10637.10637,0,0,0,.0853.12792,1.97669,1.97669,0,0,1,1.27939,1.33733,2.00266,2.00266,0,0,1-.57112,2.12652c-.05284.05166-.04168.08328-.01173.13489A3.51189,3.51189,0,0,1,19.24775,14.722Zm-6.35959-.27836a1.6984,1.6984,0,0,0,1.14556,1.61113,3.82039,3.82039,0,0,0,1.036.17935,1.46888,1.46888,0,0,0,.73509-.12255.44082.44082,0,0,0,.26057-.44274.45312.45312,0,0,0-.29211-.43375.97191.97191,0,0,0-.20678-.063c-.21326-.03806-.42754-.0701-.63973-.11215a.54787.54787,0,0,1-.50172-.60926,2.75864,2.75864,0,0,1,.1773-.901c.1763-.535.414-1.045.64183-1.55913A12.686,12.686,0,0,0,15.85,10.47863a1.58461,1.58461,0,0,0,.04861-.87208,1.04531,1.04531,0,0,0-.85432-.83981,1.60658,1.60658,0,0,0-1.23654.16594.27593.27593,0,0,1-.36286-.03413c-.085-.0747-.16594-.15379-.24918-.23055a.98682.98682,0,0,0-1.33577-.04933,6.1468,6.1468,0,0,1-.4989.41615.47762.47762,0,0,1-.51535.03566c-.17448-.09307-.35512-.175-.53531-.25665a1.74949,1.74949,0,0,0-.56476-.2016,1.69943,1.69943,0,0,0-1.61654.91787,8.05815,8.05815,0,0,0-.32952.80126c-.45471,1.2557-.82507,2.53825-1.20838,3.81639a1.24151,1.24151,0,0,0,.51532,1.44389,1.42659,1.42659,0,0,0,1.22008.17166,1.09728,1.09728,0,0,0,.66994-.69764c.44145-1.04111.839-2.09989,1.25981-3.14926.11581-.28876.22792-.57874.35078-.86438a.44548.44548,0,0,1,.69189-.19539.50521.50521,0,0,1,.15044.43836,1.75625,1.75625,0,0,1-.14731.50453c-.27379.69219-.55265,1.38236-.82766,2.074a2.0836,2.0836,0,0,0-.14038.42876.50719.50719,0,0,0,.27082.57722.87236.87236,0,0,0,.66145.02739.99137.99137,0,0,0,.53406-.532q.61571-1.20914,1.228-2.42031.28423-.55863.57585-1.1133a.87189.87189,0,0,1,.29055-.35253.34987.34987,0,0,1,.37634-.01265.30291.30291,0,0,1,.12434.31459.56716.56716,0,0,1-.04655.1915c-.05318.12739-.10286.25669-.16183.38156-.34118.71775-.68754,1.43273-1.02568,2.152A2.00213,2.00213,0,0,0,12.88816,14.44366Zm4.78568,5.28972a.88573.88573,0,0,0-1.77139.00465.8857.8857,0,0,0,1.77139-.00465Zm-14.83838-7.296a.84329.84329,0,1,0,.00827-1.68655.8433.8433,0,0,0-.00827,1.68655Zm10.366-9.43673a.83506.83506,0,1,0-.0091,1.67.83505.83505,0,0,0,.0091-1.67Zm6.85014,5.22a.71651.71651,0,0,0-1.433.0093.71656.71656,0,0,0,1.433-.0093ZM5.37528,6.17908A.63823.63823,0,1,0,6.015,5.54483.62292.62292,0,0,0,5.37528,6.17908Zm6.68214,14.80843a.54949.54949,0,1,0-.55052.541A.54556.54556,0,0,0,12.05742,20.98752Zm8.53235-8.49689a.54777.54777,0,0,0-.54027.54023.53327.53327,0,0,0,.532.52293.51548.51548,0,0,0,.53272-.5237A.53187.53187,0,0,0,20.58977,12.49063ZM7.82846,2.4715a.44927.44927,0,1,0,.44484.44766A.43821.43821,0,0,0,7.82846,2.4715Zm13.775,7.60492a.41186.41186,0,0,0-.40065.39623.40178.40178,0,0,0,.40168.40168A.38994.38994,0,0,0,22,10.48172.39946.39946,0,0,0,21.60349,10.07642ZM5.79193,17.96207a.40469.40469,0,0,0-.397-.39646.399.399,0,0,0-.396.405.39234.39234,0,0,0,.39939.389A.39857.39857,0,0,0,5.79193,17.96207Z"/>
+</symbol>
+<symbol id="icon-medium" viewBox="0 0 24 24">
+<path d="M20.962,7.257l-5.457,8.867l-3.923-6.375l3.126-5.08c0.112-0.182,0.319-0.286,0.527-0.286c0.05,0,0.1,0.008,0.149,0.02 c0.039,0.01,0.078,0.023,0.114,0.041l5.43,2.715l0.006,0.003c0.004,0.002,0.007,0.006,0.011,0.008 C20.971,7.191,20.98,7.227,20.962,7.257z M9.86,8.592v5.783l5.14,2.57L9.86,8.592z M15.772,17.331l4.231,2.115 C20.554,19.721,21,19.529,21,19.016V8.835L15.772,17.331z M8.968,7.178L3.665,4.527C3.569,4.479,3.478,4.456,3.395,4.456 C3.163,4.456,3,4.636,3,4.938v11.45c0,0.306,0.224,0.669,0.498,0.806l4.671,2.335c0.12,0.06,0.234,0.088,0.337,0.088 c0.29,0,0.494-0.225,0.494-0.602V7.231C9,7.208,8.988,7.188,8.968,7.178z"/>
+</symbol>
+<symbol id="icon-pinterest" viewBox="0 0 24 24">
+<path d="M12.289,2C6.617,2,3.606,5.648,3.606,9.622c0,1.846,1.025,4.146,2.666,4.878c0.25,0.111,0.381,0.063,0.439-0.169 c0.044-0.175,0.267-1.029,0.365-1.428c0.032-0.128,0.017-0.237-0.091-0.362C6.445,11.911,6.01,10.75,6.01,9.668 c0-2.777,2.194-5.464,5.933-5.464c3.23,0,5.49,2.108,5.49,5.122c0,3.407-1.794,5.768-4.13,5.768c-1.291,0-2.257-1.021-1.948-2.277 c0.372-1.495,1.089-3.112,1.089-4.191c0-0.967-0.542-1.775-1.663-1.775c-1.319,0-2.379,1.309-2.379,3.059 c0,1.115,0.394,1.869,0.394,1.869s-1.302,5.279-1.54,6.261c-0.405,1.666,0.053,4.368,0.094,4.604 c0.021,0.126,0.167,0.169,0.25,0.063c0.129-0.165,1.699-2.419,2.142-4.051c0.158-0.59,0.817-2.995,0.817-2.995 c0.43,0.784,1.681,1.446,3.013,1.446c3.963,0,6.822-3.494,6.822-7.833C20.394,5.112,16.849,2,12.289,2"/>
+</symbol>
+<symbol id="icon-pocket" viewBox="0 0 24 24">
+<path d="M21.927,4.194C21.667,3.48,20.982,3,20.222,3h-0.01h-1.721H3.839C3.092,3,2.411,3.47,2.145,4.17 C2.066,4.378,2.026,4.594,2.026,4.814v6.035l0.069,1.2c0.29,2.73,1.707,5.115,3.899,6.778c0.039,0.03,0.079,0.059,0.119,0.089 l0.025,0.018c1.175,0.859,2.491,1.441,3.91,1.727c0.655,0.132,1.325,0.2,1.991,0.2c0.615,0,1.232-0.057,1.839-0.17 c0.073-0.014,0.145-0.028,0.219-0.044c0.02-0.004,0.042-0.012,0.064-0.023c1.359-0.297,2.621-0.864,3.753-1.691l0.025-0.018 c0.04-0.029,0.08-0.058,0.119-0.089c2.192-1.664,3.609-4.049,3.898-6.778l0.069-1.2V4.814C22.026,4.605,22,4.398,21.927,4.194z M17.692,10.481l-4.704,4.512c-0.266,0.254-0.608,0.382-0.949,0.382c-0.342,0-0.684-0.128-0.949-0.382l-4.705-4.512 C5.838,9.957,5.82,9.089,6.344,8.542c0.524-0.547,1.392-0.565,1.939-0.04l3.756,3.601l3.755-3.601 c0.547-0.524,1.415-0.506,1.939,0.04C18.256,9.089,18.238,9.956,17.692,10.481z"/>
+</symbol>
+<symbol id="icon-reddit" viewBox="0 0 24 24">
+<path d="M22,11.816c0-1.256-1.021-2.277-2.277-2.277c-0.593,0-1.122,0.24-1.526,0.614c-1.481-0.965-3.455-1.594-5.647-1.69 l1.171-3.702l3.18,0.748c0.008,1.028,0.846,1.862,1.876,1.862c1.035,0,1.877-0.842,1.877-1.878c0-1.035-0.842-1.877-1.877-1.877 c-0.769,0-1.431,0.466-1.72,1.13l-3.508-0.826c-0.203-0.047-0.399,0.067-0.46,0.261l-1.35,4.268 c-2.316,0.038-4.411,0.67-5.97,1.671C5.368,9.765,4.853,9.539,4.277,9.539C3.021,9.539,2,10.56,2,11.816 c0,0.814,0.433,1.523,1.078,1.925c-0.037,0.221-0.061,0.444-0.061,0.672c0,3.292,4.011,5.97,8.941,5.97s8.941-2.678,8.941-5.97 c0-0.214-0.02-0.424-0.053-0.632C21.533,13.39,22,12.661,22,11.816z M18.776,4.394c0.606,0,1.1,0.493,1.1,1.1s-0.493,1.1-1.1,1.1 s-1.1-0.494-1.1-1.1S18.169,4.394,18.776,4.394z M2.777,11.816c0-0.827,0.672-1.5,1.499-1.5c0.313,0,0.598,0.103,0.838,0.269 c-0.851,0.676-1.477,1.479-1.812,2.36C2.983,12.672,2.777,12.27,2.777,11.816z M11.959,19.606c-4.501,0-8.164-2.329-8.164-5.193 S7.457,9.22,11.959,9.22s8.164,2.329,8.164,5.193S16.46,19.606,11.959,19.606z M20.636,13.001c-0.326-0.89-0.948-1.701-1.797-2.384 c0.248-0.186,0.55-0.301,0.883-0.301c0.827,0,1.5,0.673,1.5,1.5C21.223,12.299,20.992,12.727,20.636,13.001z M8.996,14.704 c-0.76,0-1.397-0.616-1.397-1.376c0-0.76,0.637-1.397,1.397-1.397c0.76,0,1.376,0.637,1.376,1.397 C10.372,14.088,9.756,14.704,8.996,14.704z M16.401,13.328c0,0.76-0.616,1.376-1.376,1.376c-0.76,0-1.399-0.616-1.399-1.376 c0-0.76,0.639-1.397,1.399-1.397C15.785,11.931,16.401,12.568,16.401,13.328z M15.229,16.708c0.152,0.152,0.152,0.398,0,0.55 c-0.674,0.674-1.727,1.002-3.219,1.002c-0.004,0-0.007-0.002-0.011-0.002c-0.004,0-0.007,0.002-0.011,0.002 c-1.492,0-2.544-0.328-3.218-1.002c-0.152-0.152-0.152-0.398,0-0.55c0.152-0.152,0.399-0.151,0.55,0 c0.521,0.521,1.394,0.775,2.669,0.775c0.004,0,0.007,0.002,0.011,0.002c0.004,0,0.007-0.002,0.011-0.002 c1.275,0,2.148-0.253,2.669-0.775C14.831,16.556,15.078,16.556,15.229,16.708z"/>
+</symbol>
+<symbol id="icon-skype" viewBox="0 0 24 24">
+<path d="M10.113,2.699c0.033-0.006,0.067-0.013,0.1-0.02c0.033,0.017,0.066,0.033,0.098,0.051L10.113,2.699z M2.72,10.223 c-0.006,0.034-0.011,0.069-0.017,0.103c0.018,0.032,0.033,0.064,0.051,0.095L2.72,10.223z M21.275,13.771 c0.007-0.035,0.011-0.071,0.018-0.106c-0.018-0.031-0.033-0.064-0.052-0.095L21.275,13.771z M13.563,21.199 c0.032,0.019,0.065,0.035,0.096,0.053c0.036-0.006,0.071-0.011,0.105-0.017L13.563,21.199z M22,16.386 c0,1.494-0.581,2.898-1.637,3.953c-1.056,1.057-2.459,1.637-3.953,1.637c-0.967,0-1.914-0.251-2.75-0.725 c0.036-0.006,0.071-0.011,0.105-0.017l-0.202-0.035c0.032,0.019,0.065,0.035,0.096,0.053c-0.543,0.096-1.099,0.147-1.654,0.147 c-1.275,0-2.512-0.25-3.676-0.743c-1.125-0.474-2.135-1.156-3.002-2.023c-0.867-0.867-1.548-1.877-2.023-3.002 c-0.493-1.164-0.743-2.401-0.743-3.676c0-0.546,0.049-1.093,0.142-1.628c0.018,0.032,0.033,0.064,0.051,0.095L2.72,10.223 c-0.006,0.034-0.011,0.069-0.017,0.103C2.244,9.5,2,8.566,2,7.615c0-1.493,0.582-2.898,1.637-3.953 c1.056-1.056,2.46-1.638,3.953-1.638c0.915,0,1.818,0.228,2.622,0.655c-0.033,0.007-0.067,0.013-0.1,0.02l0.199,0.031 c-0.032-0.018-0.066-0.034-0.098-0.051c0.002,0,0.003-0.001,0.004-0.001c0.586-0.112,1.187-0.169,1.788-0.169 c1.275,0,2.512,0.249,3.676,0.742c1.124,0.476,2.135,1.156,3.002,2.024c0.868,0.867,1.548,1.877,2.024,3.002 c0.493,1.164,0.743,2.401,0.743,3.676c0,0.575-0.054,1.15-0.157,1.712c-0.018-0.031-0.033-0.064-0.052-0.095l0.034,0.201 c0.007-0.035,0.011-0.071,0.018-0.106C21.754,14.494,22,15.432,22,16.386z M16.817,14.138c0-1.331-0.613-2.743-3.033-3.282 l-2.209-0.49c-0.84-0.192-1.807-0.444-1.807-1.237c0-0.794,0.679-1.348,1.903-1.348c2.468,0,2.243,1.696,3.468,1.696 c0.645,0,1.209-0.379,1.209-1.031c0-1.521-2.435-2.663-4.5-2.663c-2.242,0-4.63,0.952-4.63,3.488c0,1.221,0.436,2.521,2.839,3.123 l2.984,0.745c0.903,0.223,1.129,0.731,1.129,1.189c0,0.762-0.758,1.507-2.129,1.507c-2.679,0-2.307-2.062-3.743-2.062 c-0.645,0-1.113,0.444-1.113,1.078c0,1.236,1.501,2.886,4.856,2.886C15.236,17.737,16.817,16.199,16.817,14.138z"/>
+</symbol>
+<symbol id="icon-slideshare" viewBox="0 0 24 24">
+<path d="M11.738,10.232a2.142,2.142,0,0,1-.721,1.619,2.556,2.556,0,0,1-3.464,0,2.183,2.183,0,0,1,0-3.243,2.572,2.572,0,0,1,3.464,0A2.136,2.136,0,0,1,11.738,10.232Zm5.7,0a2.15,2.15,0,0,1-.715,1.619,2.563,2.563,0,0,1-3.469,0,2.183,2.183,0,0,1,0-3.243,2.58,2.58,0,0,1,3.469,0A2.144,2.144,0,0,1,17.439,10.232Zm2.555,2.045V4.7a2.128,2.128,0,0,0-.363-1.4,1.614,1.614,0,0,0-1.261-.415H5.742a1.656,1.656,0,0,0-1.278.386A2.246,2.246,0,0,0,4.129,4.7v7.643a8.212,8.212,0,0,0,1,.454q.516.193.92.318a6.847,6.847,0,0,0,.92.21q.516.085.806.125a6.615,6.615,0,0,0,.795.045l.665.006q.16,0,.642-.023t.506-.023a1.438,1.438,0,0,1,1.079.307,1.134,1.134,0,0,0,.114.1,7.215,7.215,0,0,0,.693.579q.079-1.033,1.34-.988.057,0,.415.017l.488.023q.13.006.517.011t.6-.011l.619-.051a5.419,5.419,0,0,0,.693-.1l.7-.153a5.353,5.353,0,0,0,.761-.221q.345-.131.766-.307a8.727,8.727,0,0,0,.818-.392Zm1.851-.057a10.4,10.4,0,0,1-4.225,2.862,6.5,6.5,0,0,1-.261,5.281,3.524,3.524,0,0,1-2.078,1.681,2.452,2.452,0,0,1-2.067-.17,1.915,1.915,0,0,1-.931-1.863l-.011-3.7V16.3l-.279-.068q-.188-.045-.267-.057l-.011,3.839a1.9,1.9,0,0,1-.943,1.863,2.481,2.481,0,0,1-2.078.17,3.519,3.519,0,0,1-2.067-1.7,6.546,6.546,0,0,1-.25-5.258A10.4,10.4,0,0,1,2.152,12.22a.56.56,0,0,1-.045-.715q.238-.3.681.011l.125.079a.767.767,0,0,1,.125.091V3.8a1.987,1.987,0,0,1,.534-1.4,1.7,1.7,0,0,1,1.295-.579H19.141a1.7,1.7,0,0,1,1.295.579,1.985,1.985,0,0,1,.534,1.4v7.882l.238-.17q.443-.307.681-.011a.56.56,0,0,1-.045.715Z"/>
+</symbol>
+<symbol id="icon-snapchat" viewBox="0 0 24 24">
+<path d="M12.065,2a5.526,5.526,0,0,1,3.132.892A5.854,5.854,0,0,1,17.326,5.4a5.821,5.821,0,0,1,.351,2.33q0,.612-.117,2.487a.809.809,0,0,0,.365.091,1.93,1.93,0,0,0,.664-.176,1.93,1.93,0,0,1,.664-.176,1.3,1.3,0,0,1,.729.234.7.7,0,0,1,.351.6.839.839,0,0,1-.41.7,2.732,2.732,0,0,1-.9.41,3.192,3.192,0,0,0-.9.378.728.728,0,0,0-.41.618,1.575,1.575,0,0,0,.156.56,6.9,6.9,0,0,0,1.334,1.953,5.6,5.6,0,0,0,1.881,1.315,5.875,5.875,0,0,0,1.042.3.42.42,0,0,1,.365.456q0,.911-2.852,1.341a1.379,1.379,0,0,0-.143.507,1.8,1.8,0,0,1-.182.605.451.451,0,0,1-.429.241,5.878,5.878,0,0,1-.807-.085,5.917,5.917,0,0,0-.833-.085,4.217,4.217,0,0,0-.807.065,2.42,2.42,0,0,0-.82.293,6.682,6.682,0,0,0-.755.5q-.351.267-.755.527a3.886,3.886,0,0,1-.989.436A4.471,4.471,0,0,1,11.831,22a4.307,4.307,0,0,1-1.256-.176,3.784,3.784,0,0,1-.976-.436q-.4-.26-.749-.527a6.682,6.682,0,0,0-.755-.5,2.422,2.422,0,0,0-.807-.293,4.432,4.432,0,0,0-.82-.065,5.089,5.089,0,0,0-.853.1,5,5,0,0,1-.762.1.474.474,0,0,1-.456-.241,1.819,1.819,0,0,1-.182-.618,1.411,1.411,0,0,0-.143-.521q-2.852-.429-2.852-1.341a.42.42,0,0,1,.365-.456,5.793,5.793,0,0,0,1.042-.3,5.524,5.524,0,0,0,1.881-1.315,6.789,6.789,0,0,0,1.334-1.953A1.575,1.575,0,0,0,6,12.9a.728.728,0,0,0-.41-.618,3.323,3.323,0,0,0-.9-.384,2.912,2.912,0,0,1-.9-.41.814.814,0,0,1-.41-.684.71.71,0,0,1,.338-.593,1.208,1.208,0,0,1,.716-.241,1.976,1.976,0,0,1,.625.169,2.008,2.008,0,0,0,.69.169.919.919,0,0,0,.416-.091q-.117-1.849-.117-2.474A5.861,5.861,0,0,1,6.385,5.4,5.516,5.516,0,0,1,8.625,2.819,7.075,7.075,0,0,1,12.062,2Z"/>
+</symbol>
+<symbol id="icon-soundcloud" viewBox="0 0 24 24">
+<path d="M8.9,16.1L9,14L8.9,9.5c0-0.1,0-0.1-0.1-0.1c0,0-0.1-0.1-0.1-0.1c-0.1,0-0.1,0-0.1,0.1c0,0-0.1,0.1-0.1,0.1L8.3,14l0.1,2.1 c0,0.1,0,0.1,0.1,0.1c0,0,0.1,0.1,0.1,0.1C8.8,16.3,8.9,16.3,8.9,16.1z M11.4,15.9l0.1-1.8L11.4,9c0-0.1,0-0.2-0.1-0.2 c0,0-0.1,0-0.1,0s-0.1,0-0.1,0c-0.1,0-0.1,0.1-0.1,0.2l0,0.1l-0.1,5c0,0,0,0.7,0.1,2v0c0,0.1,0,0.1,0.1,0.1c0.1,0.1,0.1,0.1,0.2,0.1 c0.1,0,0.1,0,0.2-0.1c0.1,0,0.1-0.1,0.1-0.2L11.4,15.9z M2.4,12.9L2.5,14l-0.2,1.1c0,0.1,0,0.1-0.1,0.1c0,0-0.1,0-0.1-0.1L2.1,14 l0.1-1.1C2.2,12.9,2.3,12.9,2.4,12.9C2.3,12.9,2.4,12.9,2.4,12.9z M3.1,12.2L3.3,14l-0.2,1.8c0,0.1,0,0.1-0.1,0.1 c-0.1,0-0.1,0-0.1-0.1L2.8,14L3,12.2C3,12.2,3,12.2,3.1,12.2C3.1,12.2,3.1,12.2,3.1,12.2z M3.9,11.9L4.1,14l-0.2,2.1 c0,0.1,0,0.1-0.1,0.1c-0.1,0-0.1,0-0.1-0.1L3.5,14l0.2-2.1c0-0.1,0-0.1,0.1-0.1C3.9,11.8,3.9,11.8,3.9,11.9z M4.7,11.9L4.9,14 l-0.2,2.1c0,0.1-0.1,0.1-0.1,0.1c-0.1,0-0.1,0-0.1-0.1L4.3,14l0.2-2.2c0-0.1,0-0.1,0.1-0.1C4.7,11.7,4.7,11.8,4.7,11.9z M5.6,12 l0.2,2l-0.2,2.1c0,0.1-0.1,0.1-0.1,0.1c0,0-0.1,0-0.1,0c0,0,0-0.1,0-0.1L5.1,14l0.2-2c0,0,0-0.1,0-0.1s0.1,0,0.1,0 C5.5,11.9,5.5,11.9,5.6,12L5.6,12z M6.4,10.7L6.6,14l-0.2,2.1c0,0,0,0.1,0,0.1c0,0-0.1,0-0.1,0c-0.1,0-0.1-0.1-0.2-0.2L5.9,14 l0.2-3.3c0-0.1,0.1-0.2,0.2-0.2c0,0,0.1,0,0.1,0C6.4,10.7,6.4,10.7,6.4,10.7z M7.2,10l0.2,4.1l-0.2,2.1c0,0,0,0.1,0,0.1 c0,0-0.1,0-0.1,0c-0.1,0-0.2-0.1-0.2-0.2l-0.1-2.1L6.8,10c0-0.1,0.1-0.2,0.2-0.2c0,0,0.1,0,0.1,0S7.2,9.9,7.2,10z M8,9.6L8.2,14 L8,16.1c0,0.1-0.1,0.2-0.2,0.2c-0.1,0-0.2-0.1-0.2-0.2L7.5,14l0.1-4.4c0-0.1,0-0.1,0.1-0.1c0,0,0.1-0.1,0.1-0.1c0.1,0,0.1,0,0.1,0.1 C8,9.6,8,9.6,8,9.6z M11.4,16.1L11.4,16.1L11.4,16.1z M9.7,9.6L9.8,14l-0.1,2.1c0,0.1,0,0.1-0.1,0.2s-0.1,0.1-0.2,0.1 c-0.1,0-0.1,0-0.1-0.1s-0.1-0.1-0.1-0.2L9.2,14l0.1-4.4c0-0.1,0-0.1,0.1-0.2s0.1-0.1,0.2-0.1c0.1,0,0.1,0,0.2,0.1S9.7,9.5,9.7,9.6 L9.7,9.6z M10.6,9.8l0.1,4.3l-0.1,2c0,0.1,0,0.1-0.1,0.2c0,0-0.1,0.1-0.2,0.1c-0.1,0-0.1,0-0.2-0.1c0,0-0.1-0.1-0.1-0.2L10,14 l0.1-4.3c0-0.1,0-0.1,0.1-0.2c0,0,0.1-0.1,0.2-0.1c0.1,0,0.1,0,0.2,0.1S10.6,9.7,10.6,9.8z M12.4,14l-0.1,2c0,0.1,0,0.1-0.1,0.2 c-0.1,0.1-0.1,0.1-0.2,0.1c-0.1,0-0.1,0-0.2-0.1c-0.1-0.1-0.1-0.1-0.1-0.2l-0.1-1l-0.1-1l0.1-5.5v0c0-0.1,0-0.2,0.1-0.2 c0.1,0,0.1-0.1,0.2-0.1c0,0,0.1,0,0.1,0c0.1,0,0.1,0.1,0.1,0.2L12.4,14z M22.1,13.9c0,0.7-0.2,1.3-0.7,1.7c-0.5,0.5-1.1,0.7-1.7,0.7 h-6.8c-0.1,0-0.1,0-0.2-0.1c-0.1-0.1-0.1-0.1-0.1-0.2V8.2c0-0.1,0.1-0.2,0.2-0.3c0.5-0.2,1-0.3,1.6-0.3c1.1,0,2.1,0.4,2.9,1.1 c0.8,0.8,1.3,1.7,1.4,2.8c0.3-0.1,0.6-0.2,1-0.2c0.7,0,1.3,0.2,1.7,0.7C21.8,12.6,22.1,13.2,22.1,13.9L22.1,13.9z"/>
+</symbol>
+<symbol id="icon-spotify" viewBox="0 0 24 24">
+<path d="M12,2C6.477,2,2,6.477,2,12c0,5.523,4.477,10,10,10c5.523,0,10-4.477,10-10C22,6.477,17.523,2,12,2 M16.586,16.424 c-0.18,0.295-0.563,0.387-0.857,0.207c-2.348-1.435-5.304-1.76-8.785-0.964c-0.335,0.077-0.67-0.133-0.746-0.469 c-0.077-0.335,0.132-0.67,0.469-0.746c3.809-0.871,7.077-0.496,9.713,1.115C16.673,15.746,16.766,16.13,16.586,16.424 M17.81,13.7 c-0.226,0.367-0.706,0.482-1.072,0.257c-2.687-1.652-6.785-2.131-9.965-1.166C6.36,12.917,5.925,12.684,5.8,12.273 C5.675,11.86,5.908,11.425,6.32,11.3c3.632-1.102,8.147-0.568,11.234,1.328C17.92,12.854,18.035,13.335,17.81,13.7 M17.915,10.865 c-3.223-1.914-8.54-2.09-11.618-1.156C5.804,9.859,5.281,9.58,5.131,9.086C4.982,8.591,5.26,8.069,5.755,7.919 c3.532-1.072,9.404-0.865,13.115,1.338c0.445,0.264,0.59,0.838,0.327,1.282C18.933,10.983,18.359,11.129,17.915,10.865"/>
+</symbol>
+<symbol id="icon-stackoverflow" viewBox="0 0 24 24">
+<path d="m 17.817128,20.228605 v -5.337217 h 1.771431 V 22 H 3.6 v -7.108612 h 1.771401 v 5.337217 z" />
+<path d="m 7.3267845,14.385359 8.6959295,1.817316 0.368168,-1.748385 -8.6959318,-1.817319 z m 1.1503197,-4.140944 8.0517968,3.749872 0.73617,-1.610385 -8.0518344,-3.7728517 z m 2.2315078,-3.9569154 6.832405,5.6822664 1.12738,-1.357316 -6.832576,-5.6822636 z m 4.417,-4.2099019 -1.426448,1.0581864 5.291191,7.1316119 1.426412,-1.0582745 z M 7.1427296,18.434189 h 8.8799844 v -1.7713 H 7.1427296 Z" />
+<path d="m 17.817128,20.228605 v -5.337217 h 1.771431 V 22 H 3.6 v -7.108612 h 1.771401 v 5.337217 z" />
+<path d="m 7.3267845,14.385359 8.6959295,1.817316 0.368168,-1.748385 -8.6959318,-1.817319 z m 1.1503197,-4.140944 8.0517968,3.749872 0.73617,-1.610385 -8.0518344,-3.7728517 z m 2.2315078,-3.9569154 6.832405,5.6822664 1.12738,-1.357316 -6.832576,-5.6822636 z m 4.417,-4.2099019 -1.426448,1.0581864 5.291191,7.1316119 1.426412,-1.0582745 z M 7.1427296,18.434189 h 8.8799844 v -1.7713 H 7.1427296 Z" />
+</symbol>
+<symbol id="icon-stumbleupon" viewBox="0 0 24 24">
+<path d="M12,4.294c-2.469,0-4.471,2.002-4.471,4.471v6.353c0,0.585-0.474,1.059-1.059,1.059c-0.585,0-1.059-0.474-1.059-1.059 v-2.824H2v2.941c0,2.469,2.002,4.471,4.471,4.471c2.469,0,4.471-2.002,4.471-4.471V8.765c0-0.585,0.474-1.059,1.059-1.059 s1.059,0.474,1.059,1.059v1.294l1.412,0.647l2-0.647V8.765C16.471,6.296,14.469,4.294,12,4.294z M13.059,12.353v2.882 c0,2.469,2.002,4.471,4.471,4.471S22,17.704,22,15.235v-2.824h-3.412v2.824c0,0.585-0.474,1.059-1.059,1.059 c-0.585,0-1.059-0.474-1.059-1.059v-2.882l-2,0.647L13.059,12.353z"/>
+</symbol>
+<symbol id="icon-tumblr" viewBox="0 0 24 24">
+<path d="M16.749,17.396c-0.357,0.17-1.041,0.319-1.551,0.332c-1.539,0.041-1.837-1.081-1.85-1.896V9.847h3.861V6.937h-3.847V2.039 c0,0-2.77,0-2.817,0c-0.046,0-0.127,0.041-0.138,0.144c-0.165,1.499-0.867,4.13-3.783,5.181v2.484h1.945v6.282 c0,2.151,1.587,5.206,5.775,5.135c1.413-0.024,2.982-0.616,3.329-1.126L16.749,17.396z"/>
+</symbol>
+<symbol id="icon-twitch" viewBox="0 0 24 24">
+<path d="M16.499,8.089h-1.636v4.91h1.636V8.089z M12,8.089h-1.637v4.91H12V8.089z M4.228,3.178L3,6.451v13.092h4.499V22h2.456 l2.454-2.456h3.681L21,14.636V3.178H4.228z M19.364,13.816l-2.864,2.865H12l-2.453,2.453V16.68H5.863V4.814h13.501V13.816z"/>
+</symbol>
+<symbol id="icon-twitter" viewBox="0 0 24 24">
+<path d="M22.23,5.924c-0.736,0.326-1.527,0.547-2.357,0.646c0.847-0.508,1.498-1.312,1.804-2.27 c-0.793,0.47-1.671,0.812-2.606,0.996C18.324,4.498,17.257,4,16.077,4c-2.266,0-4.103,1.837-4.103,4.103 c0,0.322,0.036,0.635,0.106,0.935C8.67,8.867,5.647,7.234,3.623,4.751C3.27,5.357,3.067,6.062,3.067,6.814 c0,1.424,0.724,2.679,1.825,3.415c-0.673-0.021-1.305-0.206-1.859-0.513c0,0.017,0,0.034,0,0.052c0,1.988,1.414,3.647,3.292,4.023 c-0.344,0.094-0.707,0.144-1.081,0.144c-0.264,0-0.521-0.026-0.772-0.074c0.522,1.63,2.038,2.816,3.833,2.85 c-1.404,1.1-3.174,1.756-5.096,1.756c-0.331,0-0.658-0.019-0.979-0.057c1.816,1.164,3.973,1.843,6.29,1.843 c7.547,0,11.675-6.252,11.675-11.675c0-0.178-0.004-0.355-0.012-0.531C20.985,7.47,21.68,6.747,22.23,5.924z"/>
+</symbol>
+<symbol id="icon-vimeo" viewBox="0 0 24 24">
+<path d="M22.396,7.164c-0.093,2.026-1.507,4.799-4.245,8.32C15.322,19.161,12.928,21,10.97,21c-1.214,0-2.24-1.119-3.079-3.359 c-0.56-2.053-1.119-4.106-1.68-6.159C5.588,9.243,4.921,8.122,4.206,8.122c-0.156,0-0.701,0.328-1.634,0.98L1.594,7.841 c1.027-0.902,2.04-1.805,3.037-2.708C6.001,3.95,7.03,3.327,7.715,3.264c1.619-0.156,2.616,0.951,2.99,3.321 c0.404,2.557,0.685,4.147,0.841,4.769c0.467,2.121,0.981,3.181,1.542,3.181c0.435,0,1.09-0.688,1.963-2.065 c0.871-1.376,1.338-2.422,1.401-3.142c0.125-1.187-0.343-1.782-1.401-1.782c-0.498,0-1.012,0.115-1.541,0.341 c1.023-3.35,2.977-4.977,5.862-4.884C21.511,3.066,22.52,4.453,22.396,7.164z"/>
+</symbol>
+<symbol id="icon-vk" viewBox="0 0 24 24">
+<path d="M22,7.1c0.2,0.4-0.4,1.5-1.6,3.1c-0.2,0.2-0.4,0.5-0.7,0.9c-0.5,0.7-0.9,1.1-0.9,1.4c-0.1,0.3-0.1,0.6,0.1,0.8 c0.1,0.1,0.4,0.4,0.8,0.9h0l0,0c1,0.9,1.6,1.7,2,2.3c0,0,0,0.1,0.1,0.1c0,0.1,0,0.1,0.1,0.3c0,0.1,0,0.2,0,0.4 c0,0.1-0.1,0.2-0.3,0.3c-0.1,0.1-0.4,0.1-0.6,0.1l-2.7,0c-0.2,0-0.4,0-0.6-0.1c-0.2-0.1-0.4-0.1-0.5-0.2l-0.2-0.1 c-0.2-0.1-0.5-0.4-0.7-0.7s-0.5-0.6-0.7-0.8c-0.2-0.2-0.4-0.4-0.6-0.6C14.8,15,14.6,15,14.4,15c0,0,0,0-0.1,0c0,0-0.1,0.1-0.2,0.2 c-0.1,0.1-0.2,0.2-0.2,0.3c-0.1,0.1-0.1,0.3-0.2,0.5c-0.1,0.2-0.1,0.5-0.1,0.8c0,0.1,0,0.2,0,0.3c0,0.1-0.1,0.2-0.1,0.2l0,0.1 c-0.1,0.1-0.3,0.2-0.6,0.2h-1.2c-0.5,0-1,0-1.5-0.2c-0.5-0.1-1-0.3-1.4-0.6s-0.7-0.5-1.1-0.7s-0.6-0.4-0.7-0.6l-0.3-0.3 c-0.1-0.1-0.2-0.2-0.3-0.3s-0.4-0.5-0.7-0.9s-0.7-1-1.1-1.6c-0.4-0.6-0.8-1.3-1.3-2.2C2.9,9.4,2.5,8.5,2.1,7.5C2,7.4,2,7.3,2,7.2 c0-0.1,0-0.1,0-0.2l0-0.1c0.1-0.1,0.3-0.2,0.6-0.2l2.9,0c0.1,0,0.2,0,0.2,0.1S5.9,6.9,5.9,7L6,7c0.1,0.1,0.2,0.2,0.3,0.3 C6.4,7.7,6.5,8,6.7,8.4C6.9,8.8,7,9,7.1,9.2l0.2,0.3c0.2,0.4,0.4,0.8,0.6,1.1c0.2,0.3,0.4,0.5,0.5,0.7s0.3,0.3,0.4,0.4 c0.1,0.1,0.3,0.1,0.4,0.1c0.1,0,0.2,0,0.3-0.1c0,0,0,0,0.1-0.1c0,0,0.1-0.1,0.1-0.2c0.1-0.1,0.1-0.3,0.1-0.5c0-0.2,0.1-0.5,0.1-0.8 c0-0.4,0-0.8,0-1.3c0-0.3,0-0.5-0.1-0.8c0-0.2-0.1-0.4-0.1-0.5L9.6,7.6C9.4,7.3,9.1,7.2,8.7,7.1C8.6,7.1,8.6,7,8.7,6.9 C8.9,6.7,9,6.6,9.1,6.5c0.4-0.2,1.2-0.3,2.5-0.3c0.6,0,1,0.1,1.4,0.1c0.1,0,0.3,0.1,0.3,0.1c0.1,0.1,0.2,0.1,0.2,0.3 c0,0.1,0.1,0.2,0.1,0.3s0,0.3,0,0.5c0,0.2,0,0.4,0,0.6c0,0.2,0,0.4,0,0.7c0,0.3,0,0.6,0,0.9c0,0.1,0,0.2,0,0.4c0,0.2,0,0.4,0,0.5 c0,0.1,0,0.3,0,0.4s0.1,0.3,0.1,0.4c0.1,0.1,0.1,0.2,0.2,0.3c0.1,0,0.1,0,0.2,0c0.1,0,0.2,0,0.3-0.1c0.1-0.1,0.2-0.2,0.4-0.4 s0.3-0.4,0.5-0.7c0.2-0.3,0.5-0.7,0.7-1.1c0.4-0.7,0.8-1.5,1.1-2.3c0-0.1,0.1-0.1,0.1-0.2c0-0.1,0.1-0.1,0.1-0.1l0,0l0.1,0 c0,0,0,0,0.1,0s0.2,0,0.2,0l3,0c0.3,0,0.5,0,0.7,0S21.9,7,21.9,7L22,7.1z"/>
+</symbol>
+<symbol id="icon-wordpress" viewBox="0 0 24 24">
+<path d="M12.158,12.786L9.46,20.625c0.806,0.237,1.657,0.366,2.54,0.366c1.047,0,2.051-0.181,2.986-0.51 c-0.024-0.038-0.046-0.079-0.065-0.124L12.158,12.786z M3.009,12c0,3.559,2.068,6.634,5.067,8.092L3.788,8.341 C3.289,9.459,3.009,10.696,3.009,12z M18.069,11.546c0-1.112-0.399-1.881-0.741-2.48c-0.456-0.741-0.883-1.368-0.883-2.109 c0-0.826,0.627-1.596,1.51-1.596c0.04,0,0.078,0.005,0.116,0.007C16.472,3.904,14.34,3.009,12,3.009 c-3.141,0-5.904,1.612-7.512,4.052c0.211,0.007,0.41,0.011,0.579,0.011c0.94,0,2.396-0.114,2.396-0.114 C7.947,6.93,8.004,7.642,7.52,7.699c0,0-0.487,0.057-1.029,0.085l3.274,9.739l1.968-5.901l-1.401-3.838 C9.848,7.756,9.389,7.699,9.389,7.699C8.904,7.67,8.961,6.93,9.446,6.958c0,0,1.484,0.114,2.368,0.114 c0.94,0,2.397-0.114,2.397-0.114c0.485-0.028,0.542,0.684,0.057,0.741c0,0-0.488,0.057-1.029,0.085l3.249,9.665l0.897-2.996 C17.841,13.284,18.069,12.316,18.069,11.546z M19.889,7.686c0.039,0.286,0.06,0.593,0.06,0.924c0,0.912-0.171,1.938-0.684,3.22 l-2.746,7.94c2.673-1.558,4.47-4.454,4.47-7.771C20.991,10.436,20.591,8.967,19.889,7.686z M12,22C6.486,22,2,17.514,2,12 C2,6.486,6.486,2,12,2c5.514,0,10,4.486,10,10C22,17.514,17.514,22,12,22z"/>
+</symbol>
+<symbol id="icon-yelp" viewBox="0 0 24 24">
+<path d="M12.271,16.718v1.417q-.011,3.257-.067,3.4a.707.707,0,0,1-.569.446,4.637,4.637,0,0,1-2.024-.424A4.609,4.609,0,0,1,7.8,20.565a.844.844,0,0,1-.19-.4.692.692,0,0,1,.044-.29,3.181,3.181,0,0,1,.379-.524q.335-.412,2.019-2.409.011,0,.669-.781a.757.757,0,0,1,.44-.274.965.965,0,0,1,.552.039.945.945,0,0,1,.418.324.732.732,0,0,1,.139.468Zm-1.662-2.8a.783.783,0,0,1-.58.781l-1.339.435q-3.067.981-3.257.981a.711.711,0,0,1-.6-.4,2.636,2.636,0,0,1-.19-.836,9.134,9.134,0,0,1,.011-1.857,3.559,3.559,0,0,1,.335-1.389.659.659,0,0,1,.625-.357,22.629,22.629,0,0,1,2.253.859q.781.324,1.283.524l.937.379a.771.771,0,0,1,.4.34A.982.982,0,0,1,10.609,13.917Zm9.213,3.313a4.467,4.467,0,0,1-1.021,1.8,4.559,4.559,0,0,1-1.512,1.417.671.671,0,0,1-.7-.078q-.156-.112-2.052-3.2l-.524-.859a.761.761,0,0,1-.128-.513.957.957,0,0,1,.217-.513.774.774,0,0,1,.926-.29q.011.011,1.327.446,2.264.736,2.7.887a2.082,2.082,0,0,1,.524.229.673.673,0,0,1,.245.68Zm-7.5-7.049q.056,1.137-.6,1.361-.647.19-1.272-.792L6.237,4.08a.7.7,0,0,1,.212-.691,5.788,5.788,0,0,1,2.314-1,5.928,5.928,0,0,1,2.5-.352.681.681,0,0,1,.547.5q.034.2.245,3.407T12.327,10.181Zm7.384,1.2a.679.679,0,0,1-.29.658q-.167.112-3.67.959-.747.167-1.015.257l.011-.022a.769.769,0,0,1-.513-.044.914.914,0,0,1-.413-.357.786.786,0,0,1,0-.971q.011-.011.836-1.137,1.394-1.908,1.673-2.275a2.423,2.423,0,0,1,.379-.435A.7.7,0,0,1,17.435,8a4.482,4.482,0,0,1,1.372,1.489,4.81,4.81,0,0,1,.9,1.868v.034Z"/>
+</symbol>
+<symbol id="icon-youtube" viewBox="0 0 24 24">
+<path d="M21.8,8.001c0,0-0.195-1.378-0.795-1.985c-0.76-0.797-1.613-0.801-2.004-0.847c-2.799-0.202-6.997-0.202-6.997-0.202 h-0.009c0,0-4.198,0-6.997,0.202C4.608,5.216,3.756,5.22,2.995,6.016C2.395,6.623,2.2,8.001,2.2,8.001S2,9.62,2,11.238v1.517 c0,1.618,0.2,3.237,0.2,3.237s0.195,1.378,0.795,1.985c0.761,0.797,1.76,0.771,2.205,0.855c1.6,0.153,6.8,0.201,6.8,0.201 s4.203-0.006,7.001-0.209c0.391-0.047,1.243-0.051,2.004-0.847c0.6-0.607,0.795-1.985,0.795-1.985s0.2-1.618,0.2-3.237v-1.517 C22,9.62,21.8,8.001,21.8,8.001z M9.935,14.594l-0.001-5.62l5.404,2.82L9.935,14.594z"/>
+</symbol>
+</defs>
+</svg>
diff --git a/plugins/jetpack/modules/tiled-gallery.php b/plugins/jetpack/modules/tiled-gallery.php
new file mode 100644
index 00000000..19e2068d
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * Module Name: Tiled Galleries
+ * Module Description: Display image galleries in a variety of elegant arrangements.
+ * First Introduced: 2.1
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Photos and Videos
+ * Feature: Appearance
+ * Sort Order: 24
+ * Additional Search Queries: gallery, tiles, tiled, grid, mosaic, images
+ */
+
+function jetpack_load_tiled_gallery() {
+ include dirname( __FILE__ ) . "/tiled-gallery/tiled-gallery.php";
+}
+
+add_action( 'jetpack_modules_loaded', 'jetpack_tiled_gallery_loaded' );
+
+function jetpack_tiled_gallery_loaded() {
+ Jetpack::enable_module_configurable( __FILE__ );
+ add_filter( 'jetpack_module_configuration_url_tiled-gallery', 'jetpack_tiled_gallery_configuration_url' );
+}
+
+/**
+ * Overrides default configuration url
+ *
+ * @uses admin_url
+ * @return string module settings URL
+ */
+function jetpack_tiled_gallery_configuration_url() {
+ return admin_url( 'options-media.php' );
+}
+
+jetpack_load_tiled_gallery();
diff --git a/plugins/jetpack/modules/tiled-gallery/math/class-constrained-array-rounding.php b/plugins/jetpack/modules/tiled-gallery/math/class-constrained-array-rounding.php
new file mode 100644
index 00000000..d01a114a
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/math/class-constrained-array-rounding.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * Lets you round the numeric elements of an array to integers while preserving their sum.
+ *
+ * Usage:
+ *
+ * Jetpack_Constrained_Array_Rounding::get_rounded_constrained_array( $bound_array )
+ * if a specific sum doesn't need to be specified for the bound array
+ *
+ * Jetpack_Constrained_Array_Rounding::get_rounded_constrained_array( $bound_array, $sum )
+ * If the sum of $bound_array must equal $sum after rounding.
+ *
+ * If $sum is less than the sum of the floor of the elements of the array, the class defaults to using the sum of the array elements.
+ */
+class Jetpack_Constrained_Array_Rounding {
+ public static function get_rounded_constrained_array( $bound_array, $sum = false ) {
+ // Convert associative arrays before working with them and convert them back before returning the values
+ $keys = array_keys( $bound_array );
+ $bound_array = array_values( $bound_array );
+
+ $bound_array_int = self::get_int_floor_array( $bound_array );
+
+ $lower_sum = array_sum( wp_list_pluck( $bound_array_int, 'floor' ) );
+ if ( ! $sum || ( $sum < $lower_sum ) ) {
+ // If value of sum is not supplied or is invalid, calculate the sum that the returned array is constrained to match
+ $sum = array_sum( $bound_array );
+ }
+ $diff_sum = $sum - $lower_sum;
+
+ self::adjust_constrained_array( $bound_array_int, $diff_sum );
+
+ $bound_array_fin = wp_list_pluck( $bound_array_int, 'floor' );
+ return array_combine( $keys, $bound_array_fin );
+ }
+
+ private static function get_int_floor_array( $bound_array ) {
+ $bound_array_int_floor = array();
+ foreach ( $bound_array as $i => $value ) {
+ $bound_array_int_floor[ $i ] = array(
+ 'floor' => (int) floor( $value ),
+ 'fraction' => $value - floor( $value ),
+ 'index' => $i,
+ );
+ }
+
+ return $bound_array_int_floor;
+ }
+
+ private static function adjust_constrained_array( &$bound_array_int, $adjustment ) {
+ usort( $bound_array_int, array( 'self', 'cmp_desc_fraction' ) );
+
+ $start = 0;
+ $end = $adjustment - 1;
+ $length = count( $bound_array_int );
+
+ for ( $i = $start; $i <= $end; $i++ ) {
+ $bound_array_int[ $i % $length ]['floor']++;
+ }
+
+ usort( $bound_array_int, array( 'self', 'cmp_asc_index' ) );
+ }
+
+ private static function cmp_desc_fraction( $a, $b ) {
+ if ( $a['fraction'] == $b['fraction'] ) {
+ return 0;
+ }
+ return $a['fraction'] > $b['fraction'] ? -1 : 1;
+ }
+
+ private static function cmp_asc_index( $a, $b ) {
+ if ( $a['index'] == $b['index'] ) {
+ return 0;
+ }
+ return $a['index'] < $b['index'] ? -1 : 1;
+ }
+}
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery.php
new file mode 100644
index 00000000..479eadc2
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery.php
@@ -0,0 +1,295 @@
+<?php
+
+// Include the class file containing methods for rounding constrained array elements.
+// Here the constrained array element is the dimension of a row, group or an image in the tiled gallery.
+require_once dirname( __FILE__ ) . '/math/class-constrained-array-rounding.php';
+
+// Layouts
+require_once dirname( __FILE__ ) . '/tiled-gallery/tiled-gallery-rectangular.php';
+require_once dirname( __FILE__ ) . '/tiled-gallery/tiled-gallery-square.php';
+require_once dirname( __FILE__ ) . '/tiled-gallery/tiled-gallery-circle.php';
+
+class Jetpack_Tiled_Gallery {
+ private static $talaveras = array( 'rectangular', 'square', 'circle', 'rectangle', 'columns' );
+
+ public function __construct() {
+ add_action( 'admin_init', array( $this, 'settings_api_init' ) );
+ add_filter( 'jetpack_gallery_types', array( $this, 'jetpack_gallery_types' ), 9 );
+ add_filter( 'jetpack_default_gallery_type', array( $this, 'jetpack_default_gallery_type' ) );
+ }
+
+ public function tiles_enabled() {
+ // Check the setting status
+ return '' != Jetpack_Options::get_option_and_ensure_autoload( 'tiled_galleries', '' );
+ }
+
+ public function set_atts( $atts ) {
+ global $post;
+
+ $this->atts = shortcode_atts(
+ array(
+ 'order' => 'ASC',
+ 'orderby' => 'menu_order ID',
+ 'id' => isset( $post->ID ) ? $post->ID : 0,
+ 'include' => '',
+ 'exclude' => '',
+ 'type' => '',
+ 'grayscale' => false,
+ 'link' => '',
+ 'columns' => 3,
+ ),
+ $atts,
+ 'gallery'
+ );
+
+ $this->atts['id'] = (int) $this->atts['id'];
+ $this->float = is_rtl() ? 'right' : 'left';
+
+ // Default to rectangular is tiled galleries are checked
+ if ( $this->tiles_enabled() && ( ! $this->atts['type'] || 'default' == $this->atts['type'] ) ) {
+ /** This filter is already documented in functions.gallery.php */
+ $this->atts['type'] = apply_filters( 'jetpack_default_gallery_type', 'rectangular' );
+ }
+
+ if ( ! $this->atts['orderby'] ) {
+ $this->atts['orderby'] = sanitize_sql_orderby( $this->atts['orderby'] );
+ if ( ! $this->atts['orderby'] ) {
+ $this->atts['orderby'] = 'menu_order ID';
+ }
+ }
+
+ if ( 'rand' == strtolower( $this->atts['order'] ) ) {
+ $this->atts['orderby'] = 'rand';
+ }
+
+ // We shouldn't have more than 20 columns.
+ if ( ! is_numeric( $this->atts['columns'] ) || 20 < $this->atts['columns'] ) {
+ $this->atts['columns'] = 3;
+ }
+ }
+
+ public function get_attachments() {
+ extract( $this->atts );
+
+ if ( ! empty( $include ) ) {
+ $include = preg_replace( '/[^0-9,]+/', '', $include );
+ $_attachments = get_posts(
+ array(
+ 'include' => $include,
+ 'post_status' => 'inherit',
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'image',
+ 'order' => $order,
+ 'orderby' => $orderby,
+ 'suppress_filters' => false,
+ )
+ );
+
+ $attachments = array();
+ foreach ( $_attachments as $key => $val ) {
+ $attachments[ $val->ID ] = $_attachments[ $key ];
+ }
+ } elseif ( 0 == $id ) {
+ // Should NEVER Happen but infinite_scroll_load_other_plugins_scripts means it does
+ // Querying with post_parent == 0 can generate stupidly memcache sets on sites with 10000's of unattached attachments as get_children puts every post in the cache.
+ // TODO Fix this properly
+ $attachments = array();
+ } elseif ( ! empty( $exclude ) ) {
+ $exclude = preg_replace( '/[^0-9,]+/', '', $exclude );
+ $attachments = get_children(
+ array(
+ 'post_parent' => $id,
+ 'exclude' => $exclude,
+ 'post_status' => 'inherit',
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'image',
+ 'order' => $order,
+ 'orderby' => $orderby,
+ 'suppress_filters' => false,
+ )
+ );
+ } else {
+ $attachments = get_children(
+ array(
+ 'post_parent' => $id,
+ 'post_status' => 'inherit',
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'image',
+ 'order' => $order,
+ 'orderby' => $orderby,
+ 'suppress_filters' => false,
+ )
+ );
+ }
+ return $attachments;
+ }
+
+ public static function default_scripts_and_styles() {
+ wp_enqueue_script(
+ 'tiled-gallery',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/tiled-gallery/tiled-gallery/tiled-gallery.min.js',
+ 'modules/tiled-gallery/tiled-gallery/tiled-gallery.js'
+ ),
+ array( 'jquery' )
+ );
+ wp_enqueue_style( 'tiled-gallery', plugins_url( 'tiled-gallery/tiled-gallery.css', __FILE__ ), array(), '2012-09-21' );
+ wp_style_add_data( 'tiled-gallery', 'rtl', 'replace' );
+ }
+
+ public function gallery_shortcode( $val, $atts ) {
+ if ( ! empty( $val ) ) { // something else is overriding post_gallery, like a custom VIP shortcode
+ return $val;
+ }
+
+ global $post;
+
+ $this->set_atts( $atts );
+
+ $attachments = $this->get_attachments();
+ if ( empty( $attachments ) ) {
+ return '';
+ }
+
+ if ( is_feed() || defined( 'IS_HTML_EMAIL' ) ) {
+ return '';
+ }
+
+ if (
+ in_array(
+ $this->atts['type'],
+ /**
+ * Filters the permissible Tiled Gallery types.
+ *
+ * @module tiled-gallery
+ *
+ * @since 3.7.0
+ *
+ * @param array Array of allowed types. Default: 'rectangular', 'square', 'circle', 'rectangle', 'columns'.
+ */
+ $talaveras = apply_filters( 'jetpack_tiled_gallery_types', self::$talaveras )
+ )
+ ) {
+ // Enqueue styles and scripts
+ self::default_scripts_and_styles();
+
+ // Generate gallery HTML
+ $gallery_class = 'Jetpack_Tiled_Gallery_Layout_' . ucfirst( $this->atts['type'] );
+ $gallery = new $gallery_class( $attachments, $this->atts['link'], $this->atts['grayscale'], (int) $this->atts['columns'] );
+ $gallery_html = $gallery->HTML();
+
+ if ( $gallery_html && class_exists( 'Jetpack' ) && class_exists( 'Jetpack_Photon' ) ) {
+ // Tiled Galleries in Jetpack require that Photon be active.
+ // If it's not active, run it just on the gallery output.
+ if ( ! in_array( 'photon', Jetpack::get_active_modules() ) && ! Jetpack::is_development_mode() ) {
+ $gallery_html = Jetpack_Photon::filter_the_content( $gallery_html );
+ }
+ }
+
+ return trim( preg_replace( '/\s+/', ' ', $gallery_html ) ); // remove any new lines from the output so that the reader parses it better
+ }
+
+ return '';
+ }
+
+ public static function gallery_already_redefined() {
+ global $shortcode_tags;
+ $redefined = false;
+ if ( ! isset( $shortcode_tags['gallery'] ) || $shortcode_tags['gallery'] !== 'gallery_shortcode' ) {
+ $redefined = true;
+ }
+ /**
+ * Filter the output of the check for another plugin or theme affecting WordPress galleries.
+ *
+ * This will let folks that replace core’s shortcode confirm feature parity with it, so Jetpack's Tiled Galleries can still work.
+ *
+ * @module tiled-gallery
+ *
+ * @since 3.1.0
+ *
+ * @param bool $redefined Does another plugin or theme already redefines the default WordPress gallery?
+ */
+ return apply_filters( 'jetpack_tiled_gallery_shortcode_redefined', $redefined );
+ }
+
+ public static function init() {
+ if ( self::gallery_already_redefined() ) {
+ return;
+ }
+
+ $gallery = new Jetpack_Tiled_Gallery();
+ add_filter( 'post_gallery', array( $gallery, 'gallery_shortcode' ), 1001, 2 );
+ }
+
+ public static function get_content_width() {
+ $tiled_gallery_content_width = Jetpack::get_content_width();
+
+ if ( ! $tiled_gallery_content_width ) {
+ $tiled_gallery_content_width = 500;
+ }
+
+ /**
+ * Filter overwriting the default content width.
+ *
+ * @module tiled-gallery
+ *
+ * @since 2.1.0
+ *
+ * @param string $tiled_gallery_content_width Default Tiled Gallery content width.
+ */
+ return apply_filters( 'tiled_gallery_content_width', $tiled_gallery_content_width );
+ }
+
+ /**
+ * Media UI integration
+ */
+ function jetpack_gallery_types( $types ) {
+ if ( get_option( 'tiled_galleries' ) && isset( $types['default'] ) ) {
+ // Tiled is set as the default, meaning that type='default'
+ // will still display the mosaic.
+ $types['thumbnails'] = $types['default'];
+ unset( $types['default'] );
+ }
+
+ $types['rectangular'] = __( 'Tiled Mosaic', 'jetpack' );
+ $types['square'] = __( 'Square Tiles', 'jetpack' );
+ $types['circle'] = __( 'Circles', 'jetpack' );
+ $types['columns'] = __( 'Tiled Columns', 'jetpack' );
+
+ return $types;
+ }
+
+ function jetpack_default_gallery_type() {
+ return ( get_option( 'tiled_galleries' ) ? 'rectangular' : 'default' );
+ }
+
+ static function get_talaveras() {
+ return self::$talaveras;
+ }
+
+ /**
+ * Add a checkbox field to the Carousel section in Settings > Media
+ * for setting tiled galleries as the default.
+ */
+ function settings_api_init() {
+ global $wp_settings_sections;
+
+ // Add the setting field [tiled_galleries] and place it in Settings > Media
+ if ( isset( $wp_settings_sections['media']['carousel_section'] ) ) {
+ $section = 'carousel_section';
+ } else {
+ $section = 'default';
+ }
+
+ add_settings_field( 'tiled_galleries', __( 'Tiled Galleries', 'jetpack' ), array( $this, 'setting_html' ), 'media', $section );
+ register_setting( 'media', 'tiled_galleries', 'esc_attr' );
+ }
+
+ function setting_html() {
+ echo '<label><input name="tiled_galleries" type="checkbox" value="1" ' .
+ checked( 1, '' != get_option( 'tiled_galleries' ), false ) . ' /> ' .
+ __( 'Display all your gallery pictures in a cool mosaic.', 'jetpack' ) . '</br></label>';
+ }
+}
+
+add_action( 'init', array( 'Jetpack_Tiled_Gallery', 'init' ) );
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/rtl/tiled-gallery-rtl.css b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/rtl/tiled-gallery-rtl.css
new file mode 100644
index 00000000..f5cab8ea
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/rtl/tiled-gallery-rtl.css
@@ -0,0 +1,96 @@
+/* This file was automatically generated on Oct 01 2015 20:17:19 */
+
+/* =Tiled Gallery Default Styles
+-------------------------------------------------------------- */
+
+.tiled-gallery {
+ clear: both;
+ margin: 0 0 20px;
+ overflow: hidden;
+}
+.tiled-gallery img {
+ margin: 2px !important; /* Ensure that this value isn't overridden by themes that give content images blanket margins */
+}
+.tiled-gallery .gallery-group {
+ float: right;
+ position: relative;
+}
+.tiled-gallery .tiled-gallery-item {
+ float: right;
+ margin: 0;
+ position: relative;
+ width: inherit; /* prevents ie8 bug with inline width styles */
+}
+.tiled-gallery .gallery-row {
+ overflow: hidden;
+}
+.tiled-gallery .tiled-gallery-item a { /* Needs to reset some properties for theme compatibility */
+ background: transparent;
+ border: none;
+ color: inherit;
+ margin: 0;
+ padding: 0;
+ text-decoration: none;
+ width: auto;
+}
+.tiled-gallery .tiled-gallery-item img,
+.tiled-gallery .tiled-gallery-item img:hover { /* Needs to reset some properties for theme compatibility */
+ background: none;
+ border: none;
+ box-shadow: none;
+ max-width: 100%;
+ padding: 0;
+ vertical-align: middle;
+}
+.tiled-gallery-caption { /* Captions */
+ background: #eee;
+ background: rgba( 255,255,255,0.8 );
+ color: #333;
+ font-size: 13px;
+ font-weight: 400;
+ overflow: hidden;
+ padding: 10px 0;
+ position: absolute;
+ bottom: 0;
+ text-indent: 10px;
+ text-overflow: ellipsis;
+ width: 100%;
+ white-space: nowrap;
+}
+.tiled-gallery .tiled-gallery-item-small .tiled-gallery-caption { /* Smaller captions */
+ font-size: 11px;
+}
+
+/* Hide galleries in widgets until they've been resized to fit.
+ Gallery widgets are almost guaranteed to need resizing, and
+ the jump is a little more obvious than galleries in content. */
+.widget-gallery .tiled-gallery-unresized {
+ visibility: hidden;
+ height: 0px;
+ overflow: hidden;
+}
+
+/* =Greyscale
+-------------------------------------------------------------- */
+
+.tiled-gallery .tiled-gallery-item img.grayscale {
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+.tiled-gallery .tiled-gallery-item img.grayscale:hover {
+ opacity: 0;
+}
+
+
+/* =Circles Layout
+-------------------------------------------------------------- */
+
+.tiled-gallery.type-circle .tiled-gallery-item img {
+ border-radius: 50% !important; /* Ensure that circles are displayed in themes that add border-radius to all images as a default */
+}
+.tiled-gallery.type-circle .tiled-gallery-caption {
+ display: none;
+ opacity: 0;
+}
+
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/carousel-container.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/carousel-container.php
new file mode 100644
index 00000000..6fb74a20
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/carousel-container.php
@@ -0,0 +1,20 @@
+<?php
+if ( defined( 'JSON_HEX_AMP' ) ) {
+ // see shortcodes/slideshow.php
+ // This is nice to have, but not strictly necessary since we use _wp_specialchars() below
+ // phpcs:ignore PHPCompatibility
+ $extra = json_encode( $this->get_container_extra_data(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT );
+} else {
+ $extra = json_encode( $this->get_container_extra_data() );
+}
+?>
+<div
+ class="tiled-gallery type-<?php echo $this->type; ?> tiled-gallery-unresized"
+ data-original-width="<?php echo esc_attr( Jetpack_Tiled_Gallery::get_content_width() ); ?>"
+ <?php if ( isset( $extra ) ) : ?>
+ data-carousel-extra='<?php echo _wp_specialchars( wp_check_invalid_utf8( $extra ), ENT_QUOTES, false, true ); ?>'
+ <?php endif; ?>
+ itemscope itemtype="http://schema.org/ImageGallery"
+ >
+ <?php $this->template( "$this->type-layout", $context ); ?>
+</div>
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/circle-layout.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/circle-layout.php
new file mode 100644
index 00000000..7c8430dd
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/circle-layout.php
@@ -0,0 +1,3 @@
+<?php
+$this->template( 'square-layout', $context );
+
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/partials/carousel-image-args.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/partials/carousel-image-args.php
new file mode 100644
index 00000000..c28d9512
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/partials/carousel-image-args.php
@@ -0,0 +1,25 @@
+<?php
+// See https://github.com/Automattic/jetpack/issues/2765
+$fuzzy_image_meta = $item->fuzzy_image_meta();
+if ( isset( $fuzzy_image_meta['keywords'] ) ) {
+ unset( $fuzzy_image_meta['keywords'] );
+}
+
+if ( defined( 'JSON_HEX_AMP' ) ) {
+ // see shortcodes/slideshow.php
+ // This is nice to have, but not strictly necessary since we use _wp_specialchars() below
+ // phpcs:ignore PHPCompatibility
+ $fuzzy_image_meta = json_encode( array_map( 'strval', $fuzzy_image_meta ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT );
+} else {
+ $fuzzy_image_meta = json_encode( array_map( 'strval', $fuzzy_image_meta ) );
+}
+?>
+data-attachment-id="<?php echo esc_attr( $item->image->ID ); ?>"
+data-orig-file="<?php echo esc_url( wp_get_attachment_url( $item->image->ID ) ); ?>"
+data-orig-size="<?php echo esc_attr( $item->meta_width() ); ?>,<?php echo esc_attr( $item->meta_height() ); ?>"
+data-comments-opened="<?php echo esc_attr( comments_open( $item->image->ID ) ); ?>"
+data-image-meta="<?php echo _wp_specialchars( wp_check_invalid_utf8( $fuzzy_image_meta ), ENT_QUOTES, false, true ); ?>"
+data-image-title="<?php echo esc_attr( htmlspecialchars( wptexturize( $item->image->post_title ) ) ); ?>"
+data-image-description="<?php echo esc_attr( htmlspecialchars( wpautop( wptexturize( $item->image->post_content ) ) ) ); ?>"
+data-medium-file="<?php echo esc_url( $item->medium_file() ); ?>"
+data-large-file="<?php echo esc_url( $item->large_file() ); ?>"
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/partials/item.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/partials/item.php
new file mode 100644
index 00000000..8bb2ae7e
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/partials/item.php
@@ -0,0 +1,65 @@
+<?php
+$add_link = 'none' !== $this->link;
+
+// We do this for accessibility. Titles without alt's break screen readers.
+if ( empty( $item->image_alt ) && ! empty( $item->image_title ) ) {
+ $item->image_alt = $item->image_title;
+}
+?>
+<div class="tiled-gallery-item
+<?php
+if ( isset( $item->size ) ) {
+ echo " tiled-gallery-item-$item->size";}
+?>
+" itemprop="associatedMedia" itemscope itemtype="http://schema.org/ImageObject">
+ <?php if ( $add_link ) : ?>
+ <a href="<?php echo $item->link; ?>" border="0" itemprop="url">
+ <?php endif; ?>
+ <meta itemprop="width" content="<?php echo esc_attr( $item->image->width ); ?>">
+ <meta itemprop="height" content="<?php echo esc_attr( $item->image->height ); ?>">
+ <img
+ <?php $this->partial( 'carousel-image-args', array( 'item' => $item ) ); ?>
+ src="<?php echo esc_url( $item->img_src ); ?>"
+ width="<?php echo esc_attr( $item->image->width ); ?>"
+ height="<?php echo esc_attr( $item->image->height ); ?>"
+ data-original-width="<?php echo esc_attr( $item->image->width ); ?>"
+ data-original-height="<?php echo esc_attr( $item->image->height ); ?>"
+ itemprop="http://schema.org/image"
+ title="<?php echo esc_attr( $item->image_title ); ?>"
+ alt="<?php echo esc_attr( $item->image_alt ); ?>"
+ style="width: <?php echo esc_attr( $item->image->width ); ?>px; height: <?php echo esc_attr( $item->image->height ); ?>px;"
+ />
+ <?php if ( $add_link ) : ?>
+ </a>
+ <?php endif; ?>
+
+ <?php if ( $this->grayscale == true ) : ?>
+ <?php if ( $add_link ) : ?>
+ <a href="<?php echo $item->link; ?>" border="0" itemprop="url">
+ <?php endif; ?>
+ <meta itemprop="width" content="<?php echo esc_attr( $item->image->width ); ?>">
+ <meta itemprop="height" content="<?php echo esc_attr( $item->image->height ); ?>">
+ <img
+ class="grayscale"
+ src="<?php echo esc_url( $item->img_src_grayscale ); ?>"
+ width="<?php echo esc_attr( $item->image->width ); ?>"
+ height="<?php echo esc_attr( $item->image->height ); ?>"
+ data-original-width="<?php echo esc_attr( $item->image->width ); ?>"
+ data-original-height="<?php echo esc_attr( $item->image->height ); ?>"
+ itemprop="http://schema.org/image"
+ title="<?php echo esc_attr( $item->image_title ); ?>"
+ align="left"
+ alt="<?php echo esc_attr( $item->image_alt ); ?>"
+ style="width: <?php echo esc_attr( $item->image->width ); ?>px; height: <?php echo esc_attr( $item->image->height ); ?>px;"
+ />
+ <?php if ( $add_link ) : ?>
+ </a>
+ <?php endif; ?>
+ <?php endif; ?>
+
+ <?php if ( trim( $item->image->post_excerpt ) ) : ?>
+ <div class="tiled-gallery-caption" itemprop="caption description">
+ <?php echo wptexturize( $item->image->post_excerpt ); ?>
+ </div>
+ <?php endif; ?>
+</div>
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/rectangular-layout.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/rectangular-layout.php
new file mode 100644
index 00000000..802f3bff
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/rectangular-layout.php
@@ -0,0 +1,31 @@
+<?php
+foreach ( $rows as $row ) : ?>
+ <div
+ class="gallery-row"
+ style="width: <?php echo esc_attr( $row->width ); ?>px; height: <?php echo esc_attr( $row->height ); ?>px;"
+ data-original-width="<?php echo esc_attr( $row->width ); ?>"
+ data-original-height="<?php echo esc_attr( $row->height ); ?>"
+
+ >
+ <?php foreach ( $row->groups as $group ) : ?>
+ <div
+ class="gallery-group images-<?php echo esc_attr( count( $group->images ) ); ?>"
+ style="width: <?php echo esc_attr( $group->width ); ?>px; height: <?php echo esc_attr( $group->height ); ?>px;"
+ data-original-width="<?php echo esc_attr( $group->width ); ?>"
+ data-original-height="<?php echo esc_attr( $group->height ); ?>"
+ >
+ <?php foreach ( $group->items( $needs_attachment_link, $grayscale ) as $item ) : ?>
+ <?php
+ $this->partial(
+ 'item',
+ array(
+ 'item' => $item,
+ 'link' => $link,
+ )
+ );
+ ?>
+ <?php endforeach; ?>
+ </div> <!-- close group -->
+ <?php endforeach; ?>
+ </div> <!-- close row -->
+<?php endforeach; ?>
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/square-layout.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/square-layout.php
new file mode 100644
index 00000000..d743c038
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/templates/square-layout.php
@@ -0,0 +1,27 @@
+<?php
+foreach ( $rows as $row ) : ?>
+ <div class="gallery-row"
+ style="width: <?php echo esc_attr( $row->width ); ?>px; height: <?php echo esc_attr( $row->height ); ?>px;"
+ data-original-width="<?php echo esc_attr( $row->width ); ?>"
+ data-original-height="<?php echo esc_attr( $row->height ); ?>"
+ >
+ <?php $add_link = 'none' !== $link; ?>
+ <?php foreach ( $row->images as $item ) : ?>
+ <div class="gallery-group"
+ style="width: <?php echo esc_attr( $row->group_size ); ?>px; height: <?php echo esc_attr( $row->group_size ); ?>px;"
+ data-original-width="<?php echo esc_attr( $row->group_size ); ?>"
+ data-original-height="<?php echo esc_attr( $row->group_size ); ?>"
+ >
+ <?php
+ $this->partial(
+ 'item',
+ array(
+ 'item' => $item,
+ 'link' => $link,
+ )
+ );
+ ?>
+ </div>
+ <?php endforeach; ?>
+ </div>
+<?php endforeach; ?>
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-circle.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-circle.php
new file mode 100644
index 00000000..1addcb91
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-circle.php
@@ -0,0 +1,8 @@
+<?php
+require_once dirname( __FILE__ ) . '/tiled-gallery-square.php';
+
+class Jetpack_Tiled_Gallery_Layout_Circle extends Jetpack_Tiled_Gallery_Layout_Square {
+ protected $type = 'circle';
+}
+
+
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-item.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-item.php
new file mode 100644
index 00000000..423f9e1c
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-item.php
@@ -0,0 +1,107 @@
+<?php
+abstract class Jetpack_Tiled_Gallery_Item {
+ public $image;
+
+ public function __construct( $attachment_image, $needs_attachment_link, $grayscale ) {
+ $this->image = $attachment_image;
+ $this->grayscale = $grayscale;
+
+ $this->image_title = $this->image->post_title;
+
+ $this->image_alt = get_post_meta( $this->image->ID, '_wp_attachment_image_alt', true );
+ // If no Alt value, use the caption
+ if ( empty( $this->image_alt ) && ! empty( $this->image->post_excerpt ) ) {
+ $this->image_alt = trim( strip_tags( $this->image->post_excerpt ) );
+ }
+ // If still no Alt value, use the title
+ if ( empty( $this->image_alt ) && ! empty( $this->image->post_title ) ) {
+ $this->image_alt = trim( strip_tags( $this->image->post_title ) );
+ }
+
+ $this->orig_file = wp_get_attachment_url( $this->image->ID );
+ $this->link = $needs_attachment_link
+ ? get_attachment_link( $this->image->ID )
+ // The filter will photonize the URL if and only if Photon is active
+ : apply_filters( 'jetpack_photon_url', $this->orig_file );
+
+ $img_args = array(
+ 'w' => $this->image->width,
+ 'h' => $this->image->height,
+ );
+ // If h and w are the same, there's a reasonably good chance the image will need cropping to avoid being stretched.
+ if ( $this->image->height == $this->image->width ) {
+ $img_args['crop'] = true;
+ }
+ // The function will always photonoize the URL (even if Photon is
+ // not active). We need to photonize the URL to set the width/height.
+ $this->img_src = jetpack_photon_url( $this->orig_file, $img_args );
+ }
+
+ public function fuzzy_image_meta() {
+ $meta = wp_get_attachment_metadata( $this->image->ID );
+ $img_meta = ( ! empty( $meta['image_meta'] ) ) ? (array) $meta['image_meta'] : array();
+ if ( ! empty( $img_meta ) ) {
+ foreach ( $img_meta as $k => $v ) {
+ if ( 'latitude' == $k || 'longitude' == $k ) {
+ unset( $img_meta[ $k ] );
+ }
+ }
+ }
+
+ return $img_meta;
+ }
+
+ public function meta_width() {
+ $meta = wp_get_attachment_metadata( $this->image->ID );
+ return isset( $meta['width'] ) ? intval( $meta['width'] ) : '';
+ }
+
+ public function meta_height() {
+ $meta = wp_get_attachment_metadata( $this->image->ID );
+ return isset( $meta['height'] ) ? intval( $meta['height'] ) : '';
+ }
+
+ public function medium_file() {
+ $medium_file_info = wp_get_attachment_image_src( $this->image->ID, 'medium' );
+ $medium_file = isset( $medium_file_info[0] ) ? $medium_file_info[0] : '';
+ return $medium_file;
+ }
+
+ public function large_file() {
+ $large_file_info = wp_get_attachment_image_src( $this->image->ID, 'large' );
+ $large_file = isset( $large_file_info[0] ) ? $large_file_info[0] : '';
+ return $large_file;
+ }
+}
+
+class Jetpack_Tiled_Gallery_Rectangular_Item extends Jetpack_Tiled_Gallery_Item {
+ public function __construct( $attachment_image, $needs_attachment_link, $grayscale ) {
+ parent::__construct( $attachment_image, $needs_attachment_link, $grayscale );
+ $this->img_src_grayscale = jetpack_photon_url( $this->img_src, array( 'filter' => 'grayscale' ) );
+
+ $this->size = 'large';
+
+ if ( $this->image->width < 250 ) {
+ $this->size = 'small';
+ }
+ }
+}
+
+class Jetpack_Tiled_Gallery_Square_Item extends Jetpack_Tiled_Gallery_Item {
+ public function __construct( $attachment_image, $needs_attachment_link, $grayscale ) {
+ parent::__construct( $attachment_image, $needs_attachment_link, $grayscale );
+ $this->img_src_grayscale = jetpack_photon_url(
+ $this->img_src,
+ array(
+ 'filter' => 'grayscale',
+ 'resize' => array(
+ $this->image->width,
+ $this->image->height,
+ ),
+ )
+ );
+ }
+}
+
+class Jetpack_Tiled_Gallery_Circle_Item extends Jetpack_Tiled_Gallery_Square_Item {
+}
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-layout.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-layout.php
new file mode 100644
index 00000000..acaa0088
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-layout.php
@@ -0,0 +1,110 @@
+<?php
+abstract class Jetpack_Tiled_Gallery_Layout {
+ // Template whitelist
+ private static $templates = array( 'carousel-container', 'circle-layout', 'rectangular-layout', 'square-layout' );
+ private static $partials = array( 'carousel-image-args', 'item' );
+
+ protected $type; // Defined in child classes
+ public $attachments;
+ public $link;
+ public $grayscale;
+ public $columns;
+ public function __construct( $attachments, $link, $grayscale, $columns ) {
+
+ $this->attachments = $attachments;
+ $this->link = $link;
+ $this->needs_attachment_link = ! ( isset( $link ) && $link == 'file' );
+ $this->grayscale = $grayscale;
+ $this->columns = $columns;
+ }
+
+ public function HTML( $context = array() ) {
+ // Render the carousel container template, which will take the
+ // appropriate strategy to fill it
+ ob_start();
+ $this->template(
+ 'carousel-container',
+ array_merge(
+ $context,
+ array(
+ 'attachments' => $this->attachments,
+ 'link' => $this->link,
+ 'needs_attachment_link' => $this->needs_attachment_link,
+ 'grayscale' => $this->grayscale,
+ )
+ )
+ );
+ $html = ob_get_clean();
+
+ return $html;
+ }
+
+ private function template( $name, $context = null ) {
+ if ( ! in_array( $name, self::$templates ) ) {
+ return;
+ }
+
+ if ( isset( $context ) ) {
+ extract( $context );
+ }
+
+ /**
+ * Filters the Tiled Gallery template path
+ *
+ * @module tiled-gallery
+ * @since 4.4.0
+ *
+ * @param string $path Template path.
+ * @param string $path Template name.
+ * @param array $context Context array passed to the template.
+ */
+ require apply_filters( 'jetpack_tiled_gallery_template', dirname( __FILE__ ) . "/templates/$name.php", $name, $context );
+ }
+
+ private function partial( $name, $context = null ) {
+ if ( ! in_array( $name, self::$partials ) ) {
+ return;
+ }
+
+ if ( isset( $context ) ) {
+ extract( $context );
+ }
+
+ /**
+ * Filters the Tiled Gallery partial path
+ *
+ * @module tiled-gallery
+ * @since 4.4.0
+ *
+ * @param string $path Partial path.
+ * @param string $path Partial name.
+ * @param array $context Context array passed to the partial.
+ */
+ require apply_filters( 'jetpack_tiled_gallery_partial', dirname( __FILE__ ) . "/templates/partials/$name.php", $name, $context );
+ }
+
+ protected function get_container_extra_data() {
+ global $post;
+
+ $blog_id = (int) get_current_blog_id();
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $likes_blog_id = $blog_id;
+ } else {
+ $likes_blog_id = Jetpack_Options::get_option( 'id' );
+ }
+
+ if ( class_exists( 'Jetpack_Carousel' ) || in_array( 'carousel', Jetpack::get_active_modules() ) || 'carousel' == $this->link ) {
+ $extra_data = array(
+ 'blog_id' => $blog_id,
+ 'permalink' => get_permalink( isset( $post->ID ) ? $post->ID : 0 ),
+ 'likes_blog_id' => $likes_blog_id,
+ );
+ } else {
+ $extra_data = null;
+ }
+
+ return $extra_data;
+ }
+}
+
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-rectangular.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-rectangular.php
new file mode 100644
index 00000000..9ea272fc
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-rectangular.php
@@ -0,0 +1,223 @@
+<?php
+require_once dirname( __FILE__ ) . '/tiled-gallery-layout.php';
+require_once dirname( __FILE__ ) . '/tiled-gallery-shape.php';
+require_once dirname( __FILE__ ) . '/tiled-gallery-item.php';
+
+class Jetpack_Tiled_Gallery_Layout_Rectangular extends Jetpack_Tiled_Gallery_Layout {
+ protected $type = 'rectangular';
+
+ public function HTML( $context = array() ) {
+ $grouper = new Jetpack_Tiled_Gallery_Grouper( $this->attachments );
+ Jetpack_Tiled_Gallery_Shape::reset_last_shape();
+
+ return parent::HTML( array( 'rows' => $grouper->grouped_images ) );
+ }
+}
+
+class Jetpack_Tiled_Gallery_Layout_Columns extends Jetpack_Tiled_Gallery_Layout {
+ protected $type = 'rectangular'; // It doesn't need separate template for now
+
+ public function HTML( $context = array() ) {
+ $grouper = new Jetpack_Tiled_Gallery_Grouper( $this->attachments, array( 'Three_Columns', 'Two' ) );
+
+ return parent::HTML( array( 'rows' => $grouper->grouped_images ) );
+ }
+}
+
+// Alias
+class Jetpack_Tiled_Gallery_Layout_Rectangle extends Jetpack_Tiled_Gallery_Layout_Rectangular {}
+
+// Image grouping and HTML generation logic
+class Jetpack_Tiled_Gallery_Grouper {
+ public $margin = 4;
+
+ // This list is ordered. If you put a shape that's likely to occur on top, it will happen all the time.
+ public $shapes = array(
+ 'Reverse_Symmetric_Row',
+ 'Long_Symmetric_Row',
+ 'Symmetric_Row',
+ 'One_Three',
+ 'Three_One',
+ 'One_Two',
+ 'Five',
+ 'Four',
+ 'Three',
+ 'Two_One',
+ 'Panoramic',
+ );
+
+ public function __construct( $attachments, $shapes = array() ) {
+ $content_width = Jetpack_Tiled_Gallery::get_content_width();
+
+ $this->overwrite_shapes( $shapes );
+ $this->last_shape = '';
+ $this->images = $this->get_images_with_sizes( $attachments );
+ $this->grouped_images = $this->get_grouped_images();
+ $this->apply_content_width( $content_width );
+ }
+
+ public function overwrite_shapes( $shapes ) {
+ if ( ! empty( $shapes ) ) {
+ $this->shapes = $shapes;
+ }
+ }
+
+ public function get_current_row_size() {
+ $images_left = count( $this->images );
+ if ( $images_left < 3 ) {
+ return array_fill( 0, $images_left, 1 );
+ }
+
+ foreach ( $this->shapes as $shape_name ) {
+ $class_name = "Jetpack_Tiled_Gallery_$shape_name";
+ $shape = new $class_name( $this->images );
+ if ( $shape->is_possible() ) {
+ Jetpack_Tiled_Gallery_Shape::set_last_shape( $class_name );
+ return $shape->shape;
+ }
+ }
+
+ Jetpack_Tiled_Gallery_Shape::set_last_shape( 'Two' );
+ return array( 1, 1 );
+ }
+
+ public function get_images_with_sizes( $attachments ) {
+ $images_with_sizes = array();
+
+ foreach ( $attachments as $image ) {
+ $meta = wp_get_attachment_metadata( $image->ID );
+ $image->width_orig = ( isset( $meta['width'] ) && $meta['width'] > 0 ) ? $meta['width'] : 1;
+ $image->height_orig = ( isset( $meta['height'] ) && $meta['height'] > 0 ) ? $meta['height'] : 1;
+ $image->ratio = $image->width_orig / $image->height_orig;
+ $image->ratio = $image->ratio ? $image->ratio : 1;
+ $images_with_sizes[] = $image;
+ }
+
+ return $images_with_sizes;
+ }
+
+ public function read_row() {
+ $vector = $this->get_current_row_size();
+
+ $row = array();
+ foreach ( $vector as $group_size ) {
+ $row[] = new Jetpack_Tiled_Gallery_Group( array_splice( $this->images, 0, $group_size ) );
+ }
+
+ return $row;
+ }
+
+ public function get_grouped_images() {
+ $grouped_images = array();
+
+ while ( ! empty( $this->images ) ) {
+ $grouped_images[] = new Jetpack_Tiled_Gallery_Row( $this->read_row() );
+ }
+
+ return $grouped_images;
+ }
+
+ // todo: split in functions
+ // todo: do not stretch images
+ public function apply_content_width( $width ) {
+ foreach ( $this->grouped_images as $row ) {
+ $row->width = $width;
+ $row->raw_height = 1 / $row->ratio * ( $width - $this->margin * ( count( $row->groups ) - $row->weighted_ratio ) );
+ $row->height = round( $row->raw_height );
+
+ $this->calculate_group_sizes( $row );
+ }
+ }
+
+ public function calculate_group_sizes( $row ) {
+ // Storing the calculated group heights in an array for rounding them later while preserving their sum
+ // This fixes the rounding error that can lead to a few ugly pixels sticking out in the gallery
+ $group_widths_array = array();
+ foreach ( $row->groups as $group ) {
+ $group->height = $row->height;
+ // Storing the raw calculations in a separate property to prevent rounding errors from cascading down and for diagnostics
+ $group->raw_width = ( $row->raw_height - $this->margin * count( $group->images ) ) * $group->ratio + $this->margin;
+ $group_widths_array[] = $group->raw_width;
+ }
+ $rounded_group_widths_array = Jetpack_Constrained_Array_Rounding::get_rounded_constrained_array( $group_widths_array, $row->width );
+
+ foreach ( $row->groups as $group ) {
+ $group->width = array_shift( $rounded_group_widths_array );
+ $this->calculate_image_sizes( $group );
+ }
+ }
+
+ public function calculate_image_sizes( $group ) {
+ // Storing the calculated image heights in an array for rounding them later while preserving their sum
+ // This fixes the rounding error that can lead to a few ugly pixels sticking out in the gallery
+ $image_heights_array = array();
+ foreach ( $group->images as $image ) {
+ $image->width = $group->width - $this->margin;
+ // Storing the raw calculations in a separate property for diagnostics
+ $image->raw_height = ( $group->raw_width - $this->margin ) / $image->ratio;
+ $image_heights_array[] = $image->raw_height;
+ }
+
+ $image_height_sum = $group->height - count( $image_heights_array ) * $this->margin;
+ $rounded_image_heights_array = Jetpack_Constrained_Array_Rounding::get_rounded_constrained_array( $image_heights_array, $image_height_sum );
+
+ foreach ( $group->images as $image ) {
+ $image->height = array_shift( $rounded_image_heights_array );
+ }
+ }
+}
+
+class Jetpack_Tiled_Gallery_Row {
+ public function __construct( $groups ) {
+ $this->groups = $groups;
+ $this->ratio = $this->get_ratio();
+ $this->weighted_ratio = $this->get_weighted_ratio();
+ }
+
+ public function get_ratio() {
+ $ratio = 0;
+ foreach ( $this->groups as $group ) {
+ $ratio += $group->ratio;
+ }
+ return $ratio > 0 ? $ratio : 1;
+ }
+
+ public function get_weighted_ratio() {
+ $weighted_ratio = 0;
+ foreach ( $this->groups as $group ) {
+ $weighted_ratio += $group->ratio * count( $group->images );
+ }
+ return $weighted_ratio > 0 ? $weighted_ratio : 1;
+ }
+}
+
+class Jetpack_Tiled_Gallery_Group {
+ public function __construct( $images ) {
+ $this->images = $images;
+ $this->ratio = $this->get_ratio();
+ }
+
+ public function get_ratio() {
+ $ratio = 0;
+ foreach ( $this->images as $image ) {
+ if ( $image->ratio ) {
+ $ratio += 1 / $image->ratio;
+ }
+ }
+ if ( ! $ratio ) {
+ return 1;
+ }
+
+ return 1 / $ratio;
+ }
+
+ public function items( $needs_attachment_link, $grayscale ) {
+ $items = array();
+ foreach ( $this->images as $image ) {
+ $items[] = new Jetpack_Tiled_Gallery_Rectangular_Item( $image, $needs_attachment_link, $grayscale );
+ }
+
+ return $items;
+ }
+}
+
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-rtl.css b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-rtl.css
new file mode 100644
index 00000000..34e50334
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-rtl.css
@@ -0,0 +1 @@
+.tiled-gallery{clear:both;margin:0 0 20px;overflow:hidden}.tiled-gallery img{margin:2px!important}.tiled-gallery .gallery-group{float:right;position:relative}.tiled-gallery .tiled-gallery-item{float:right;margin:0;position:relative;width:inherit}.tiled-gallery .gallery-row{overflow:hidden}.tiled-gallery .tiled-gallery-item a{background:100% 0;border:none;color:inherit;margin:0;padding:0;text-decoration:none;width:auto}.tiled-gallery .tiled-gallery-item img,.tiled-gallery .tiled-gallery-item img:hover{background:100% 0;border:none;box-shadow:none;max-width:100%;padding:0;vertical-align:middle}.tiled-gallery-caption{background:#eee;background:rgba(255,255,255,.8);color:#333;font-size:13px;font-weight:400;overflow:hidden;padding:10px 0;position:absolute;bottom:0;text-indent:10px;text-overflow:ellipsis;width:100%;white-space:nowrap}.tiled-gallery .tiled-gallery-item-small .tiled-gallery-caption{font-size:11px}.widget-gallery .tiled-gallery-unresized{visibility:hidden;height:0;overflow:hidden}.tiled-gallery .tiled-gallery-item img.grayscale{position:absolute;right:0;top:0}.tiled-gallery .tiled-gallery-item img.grayscale:hover{opacity:0}.tiled-gallery.type-circle .tiled-gallery-item img{border-radius:50%!important}.tiled-gallery.type-circle .tiled-gallery-caption{display:none;opacity:0} \ No newline at end of file
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-shape.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-shape.php
new file mode 100644
index 00000000..bc243966
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-shape.php
@@ -0,0 +1,209 @@
+<?php
+class Jetpack_Tiled_Gallery_Shape {
+ static $shapes_used = array();
+
+ public function __construct( $images ) {
+ $this->images = $images;
+ $this->images_left = count( $images );
+ }
+
+ public function sum_ratios( $number_of_images = 3 ) {
+ return array_sum( array_slice( wp_list_pluck( $this->images, 'ratio' ), 0, $number_of_images ) );
+ }
+
+ public function next_images_are_symmetric() {
+ return $this->images_left > 2 && $this->images[0]->ratio == $this->images[2]->ratio;
+ }
+
+ public function is_not_as_previous( $n = 1 ) {
+ return ! in_array( get_class( $this ), array_slice( self::$shapes_used, -$n ) );
+ }
+
+ public function is_wide_theme() {
+ return Jetpack::get_content_width() > 1000;
+ }
+
+ public function image_is_landscape( $image ) {
+ return $image->ratio >= 1 && $image->ratio < 2;
+ }
+
+ public function image_is_portrait( $image ) {
+ return $image->ratio < 1;
+ }
+
+ public function image_is_panoramic( $image ) {
+ return $image->ratio >= 2;
+ }
+
+ public static function set_last_shape( $last_shape ) {
+ self::$shapes_used[] = $last_shape;
+ }
+
+ public static function reset_last_shape() {
+ self::$shapes_used = array();
+ }
+}
+
+class Jetpack_Tiled_Gallery_Three extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 1, 1, 1 );
+
+ public function is_possible() {
+ $ratio = $this->sum_ratios( 3 );
+ $has_enough_images = $this->images_left >= 3 && ! in_array( $this->images_left, array( 4, 6 ) );
+ return $has_enough_images && $this->is_not_as_previous( 3 ) &&
+ ( ( $ratio < 2.5 ) || ( $ratio < 5 && $this->next_images_are_symmetric() ) || $this->is_wide_theme() );
+ }
+}
+
+class Jetpack_Tiled_Gallery_Four extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 1, 1, 1, 1 );
+
+ public function is_possible() {
+ return $this->is_not_as_previous() &&
+ (
+ ( $this->sum_ratios( 4 ) < 3.5 && $this->images_left > 5 ) ||
+ ( $this->sum_ratios( 4 ) < 7 && $this->images_left == 4 )
+ );
+ }
+}
+
+class Jetpack_Tiled_Gallery_Five extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 1, 1, 1, 1, 1 );
+
+ public function is_possible() {
+ return $this->is_wide_theme() && $this->is_not_as_previous() && $this->sum_ratios( 5 ) < 5 &&
+ ( $this->images_left == 5 || ( $this->images_left != 10 && $this->images_left > 6 ) );
+ }
+}
+
+class Jetpack_Tiled_Gallery_Two_One extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 2, 1 );
+
+ public function is_possible() {
+ return $this->is_not_as_previous( 3 ) && $this->images_left >= 2 &&
+ $this->images[2]->ratio < 1.6 && $this->images[0]->ratio >= 0.9 && $this->images[0]->ratio < 2.0 && $this->images[1]->ratio >= 0.9 && $this->images[1]->ratio < 2.0;
+ }
+}
+
+class Jetpack_Tiled_Gallery_One_Two extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 1, 2 );
+
+ public function is_possible() {
+ return $this->is_not_as_previous( 3 ) && $this->images_left >= 2 &&
+ $this->images[0]->ratio < 1.6 && $this->images[1]->ratio >= 0.9 && $this->images[1]->ratio < 2.0 && $this->images[2]->ratio >= 0.9 && $this->images[2]->ratio < 2.0;
+ }
+}
+
+class Jetpack_Tiled_Gallery_One_Three extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 1, 3 );
+
+ public function is_possible() {
+ return $this->is_not_as_previous( 3 ) && $this->images_left > 3 &&
+ $this->image_is_portrait( $this->images[0] ) &&
+ $this->image_is_landscape( $this->images[1] ) &&
+ $this->image_is_landscape( $this->images[2] ) &&
+ $this->image_is_landscape( $this->images[3] );
+ }
+}
+
+class Jetpack_Tiled_Gallery_Three_One extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 3, 1 );
+
+ public function is_possible() {
+ return $this->is_not_as_previous( 3 ) && $this->images_left > 3 &&
+ $this->image_is_portrait( $this->images[3] ) &&
+ $this->image_is_landscape( $this->images[0] ) &&
+ $this->image_is_landscape( $this->images[1] ) &&
+ $this->image_is_landscape( $this->images[2] );
+ }
+}
+
+class Jetpack_Tiled_Gallery_Panoramic extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 1 );
+
+ public function is_possible() {
+ return $this->image_is_panoramic( $this->images[0] );
+ }
+}
+
+class Jetpack_Tiled_Gallery_Symmetric_Row extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 1, 2, 1 );
+
+ public function is_possible() {
+ return $this->is_not_as_previous( 5 ) &&
+ $this->images_left > 3 &&
+ $this->images_left != 5 &&
+ $this->image_is_portrait( $this->images[0] ) &&
+ $this->image_is_landscape( $this->images[1] ) &&
+ $this->image_is_landscape( $this->images[2] ) &&
+ $this->image_is_portrait( $this->images[3] );
+ }
+}
+class Jetpack_Tiled_Gallery_Reverse_Symmetric_Row extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 2, 1, 2 );
+
+ public function is_possible() {
+ return $this->is_not_as_previous( 5 ) && $this->images_left > 15 &&
+ $this->image_is_landscape( $this->images[0] ) &&
+ $this->image_is_landscape( $this->images[1] ) &&
+ $this->image_is_portrait( $this->images[2] ) &&
+ $this->image_is_landscape( $this->images[3] ) &&
+ $this->image_is_landscape( $this->images[4] );
+ }
+}
+
+class Jetpack_Tiled_Gallery_Long_Symmetric_Row extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array( 3, 1, 3 );
+
+ public function is_possible() {
+ return $this->is_not_as_previous( 5 ) && $this->images_left > 15 &&
+ $this->image_is_landscape( $this->images[0] ) &&
+ $this->image_is_landscape( $this->images[1] ) &&
+ $this->image_is_landscape( $this->images[2] ) &&
+ $this->image_is_portrait( $this->images[3] ) &&
+ $this->image_is_landscape( $this->images[4] ) &&
+ $this->image_is_landscape( $this->images[5] ) &&
+ $this->image_is_landscape( $this->images[6] );
+ }
+}
+
+class Jetpack_Tiled_Gallery_Three_Columns extends Jetpack_Tiled_Gallery_Shape {
+ public $shape = array();
+
+ public function __construct( $images ) {
+ parent::__construct( $images );
+
+ $total_ratio = $this->sum_ratios( $this->images_left );
+ $approximate_column_ratio = $total_ratio / 3;
+ $column_one_images = $column_two_images = $column_three_images = $sum = 0;
+
+ foreach ( $this->images as $image ) {
+ if ( $sum <= $approximate_column_ratio ) {
+ $column_one_images++;
+ }
+
+ if ( $sum > $approximate_column_ratio && $sum <= 2 * $approximate_column_ratio ) {
+ $column_two_images++;
+ }
+ $sum += $image->ratio;
+ }
+
+ $column_three_images = $this->images_left - $column_two_images - $column_one_images;
+
+ if ( $column_one_images ) {
+ $this->shape[] = $column_one_images;
+ }
+
+ if ( $column_two_images ) {
+ $this->shape[] = $column_two_images;
+ }
+
+ if ( $column_three_images ) {
+ $this->shape[] = $column_three_images;
+ }
+ }
+
+ public function is_possible() {
+ return ! empty( $this->shape );
+ }
+}
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-square.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-square.php
new file mode 100644
index 00000000..735c19eb
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-square.php
@@ -0,0 +1,70 @@
+<?php
+require_once dirname( __FILE__ ) . '/tiled-gallery-layout.php';
+require_once dirname( __FILE__ ) . '/tiled-gallery-item.php';
+
+class Jetpack_Tiled_Gallery_Layout_Square extends Jetpack_Tiled_Gallery_Layout {
+ protected $type = 'square';
+
+ private function compute_items() {
+ $content_width = Jetpack_Tiled_Gallery::get_content_width();
+ $images_per_row = ( $this->columns > 1 ? $this->columns : 1 );
+ $margin = 2;
+
+ $margin_space = ( $images_per_row * $margin ) * 2;
+ $size = floor( ( $content_width - $margin_space ) / $images_per_row );
+ $img_size = $remainder_size = $size;
+ $remainder = count( $this->attachments ) % $images_per_row;
+ if ( $remainder > 0 ) {
+ $remainder_space = ( $remainder * $margin ) * 2;
+ $remainder_size = floor( ( $content_width - $remainder_space ) / $remainder );
+ }
+
+ $c = 1;
+ $items_in_row = 0;
+ $rows = array();
+ $row = new stdClass();
+ $row->images = array();
+ foreach ( $this->attachments as $image ) {
+ if ( $remainder > 0 && $c <= $remainder ) {
+ $img_size = $remainder_size;
+ } else {
+ $img_size = $size;
+ }
+
+ $image->width = $image->height = $img_size;
+
+ $item = new Jetpack_Tiled_Gallery_Square_Item( $image, $this->needs_attachment_link, $this->grayscale );
+
+ $row->images[] = $item;
+ $c ++;
+ $items_in_row++;
+
+ if ( $images_per_row === $items_in_row || $remainder + 1 == $c ) {
+ $rows[] = $row;
+ $items_in_row = 0;
+
+ $row->height = $img_size + $margin * 2;
+ $row->width = $content_width;
+ $row->group_size = $img_size + 2 * $margin;
+
+ $row = new stdClass();
+ $row->images = array();
+ }
+ }
+
+ if ( ! empty( $row->images ) ) {
+ $row->height = $img_size + $margin * 2;
+ $row->width = $content_width;
+ $row->group_size = $img_size + 2 * $margin;
+
+ $rows[] = $row;
+ }
+
+ return $rows;
+ }
+
+ public function HTML( $context = array() ) {
+ return parent::HTML( array( 'rows' => $this->compute_items() ) );
+ }
+}
+
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery.css b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery.css
new file mode 100644
index 00000000..b4cdc576
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery.css
@@ -0,0 +1,94 @@
+/* =Tiled Gallery Default Styles
+-------------------------------------------------------------- */
+
+.tiled-gallery {
+ clear: both;
+ margin: 0 0 20px;
+ overflow: hidden;
+}
+.tiled-gallery img {
+ margin: 2px !important; /* Ensure that this value isn't overridden by themes that give content images blanket margins */
+}
+.tiled-gallery .gallery-group {
+ float: left;
+ position: relative;
+}
+.tiled-gallery .tiled-gallery-item {
+ float: left;
+ margin: 0;
+ position: relative;
+ width: inherit; /* prevents ie8 bug with inline width styles */
+}
+.tiled-gallery .gallery-row {
+ overflow: hidden;
+}
+.tiled-gallery .tiled-gallery-item a { /* Needs to reset some properties for theme compatibility */
+ background: transparent;
+ border: none;
+ color: inherit;
+ margin: 0;
+ padding: 0;
+ text-decoration: none;
+ width: auto;
+}
+.tiled-gallery .tiled-gallery-item img,
+.tiled-gallery .tiled-gallery-item img:hover { /* Needs to reset some properties for theme compatibility */
+ background: none;
+ border: none;
+ box-shadow: none;
+ max-width: 100%;
+ padding: 0;
+ vertical-align: middle;
+}
+.tiled-gallery-caption { /* Captions */
+ background: #eee;
+ background: rgba( 255,255,255,0.8 );
+ color: #333;
+ font-size: 13px;
+ font-weight: 400;
+ overflow: hidden;
+ padding: 10px 0;
+ position: absolute;
+ bottom: 0;
+ text-indent: 10px;
+ text-overflow: ellipsis;
+ width: 100%;
+ white-space: nowrap;
+}
+.tiled-gallery .tiled-gallery-item-small .tiled-gallery-caption { /* Smaller captions */
+ font-size: 11px;
+}
+
+/* Hide galleries in widgets until they've been resized to fit.
+ Gallery widgets are almost guaranteed to need resizing, and
+ the jump is a little more obvious than galleries in content. */
+.widget-gallery .tiled-gallery-unresized {
+ visibility: hidden;
+ height: 0px;
+ overflow: hidden;
+}
+
+/* =Greyscale
+-------------------------------------------------------------- */
+
+.tiled-gallery .tiled-gallery-item img.grayscale {
+ position: absolute;
+ left: 0;
+ top: 0;
+}
+.tiled-gallery .tiled-gallery-item img.grayscale:hover {
+ opacity: 0;
+}
+
+
+/* =Circles Layout
+-------------------------------------------------------------- */
+
+.tiled-gallery.type-circle .tiled-gallery-item img {
+ border-radius: 50% !important; /* Ensure that circles are displayed in themes that add border-radius to all images as a default */
+}
+.tiled-gallery.type-circle .tiled-gallery-caption {
+ display: none;
+ opacity: 0;
+}
+
diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery.js b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery.js
new file mode 100644
index 00000000..087ff3f1
--- /dev/null
+++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery.js
@@ -0,0 +1,191 @@
+/* jshint onevar:false, smarttabs:true */
+
+( function( $ ) {
+ function TiledGalleryCollection() {
+ this.galleries = [];
+ this.findAndSetupNewGalleries();
+ }
+
+ TiledGalleryCollection.prototype.findAndSetupNewGalleries = function() {
+ var self = this;
+ $( '.tiled-gallery.tiled-gallery-unresized' ).each( function() {
+ self.galleries.push( new TiledGallery( $( this ) ) );
+ } );
+ };
+
+ TiledGalleryCollection.prototype.resizeAll = function() {
+ $.each( this.galleries, function( i, gallery ) {
+ gallery.resize();
+ } );
+ };
+
+ function TiledGallery( galleryElem ) {
+ this.gallery = galleryElem;
+
+ this.addCaptionEvents();
+
+ // Resize when initialized to fit the gallery to window dimensions
+ this.resize();
+
+ // Displays the gallery and prevents it from being initialized again
+ this.gallery.removeClass( 'tiled-gallery-unresized' );
+ }
+
+ /**
+ * Selector for all resizeable elements inside a Tiled Gallery
+ */
+
+ TiledGallery.prototype.resizeableElementsSelector =
+ '.gallery-row, .gallery-group, .tiled-gallery-item img';
+
+ /**
+ * Story
+ */
+
+ TiledGallery.prototype.addCaptionEvents = function() {
+ // Hide captions
+ this.gallery.find( '.tiled-gallery-caption' ).hide();
+
+ // Add hover effects to bring the caption up and down for each item
+ this.gallery.find( '.tiled-gallery-item' ).hover(
+ function() {
+ $( this )
+ .find( '.tiled-gallery-caption' )
+ .stop( true, true )
+ .slideDown( 'fast' );
+ },
+ function() {
+ $( this )
+ .find( '.tiled-gallery-caption' )
+ .stop( true, true )
+ .slideUp( 'fast' );
+ }
+ );
+ };
+
+ TiledGallery.prototype.getExtraDimension = function( el, attribute, mode ) {
+ if ( mode === 'horizontal' ) {
+ var left = attribute === 'border' ? 'borderLeftWidth' : attribute + 'Left';
+ var right = attribute === 'border' ? 'borderRightWidth' : attribute + 'Right';
+ return ( parseInt( el.css( left ), 10 ) || 0 ) + ( parseInt( el.css( right ), 10 ) || 0 );
+ } else if ( mode === 'vertical' ) {
+ var top = attribute === 'border' ? 'borderTopWidth' : attribute + 'Top';
+ var bottom = attribute === 'border' ? 'borderBottomWidth' : attribute + 'Bottom';
+ return ( parseInt( el.css( top ), 10 ) || 0 ) + ( parseInt( el.css( bottom ), 10 ) || 0 );
+ } else {
+ return 0;
+ }
+ };
+
+ TiledGallery.prototype.resize = function() {
+ // Resize everything in the gallery based on the ratio of the current content width
+ // to the original content width;
+ var originalWidth = this.gallery.data( 'original-width' );
+ var currentWidth = this.gallery.parent().width();
+ var resizeRatio = Math.min( 1, currentWidth / originalWidth );
+
+ var self = this;
+ this.gallery.find( this.resizeableElementsSelector ).each( function() {
+ var thisGalleryElement = $( this );
+
+ var marginWidth = self.getExtraDimension( thisGalleryElement, 'margin', 'horizontal' );
+ var marginHeight = self.getExtraDimension( thisGalleryElement, 'margin', 'vertical' );
+
+ var paddingWidth = self.getExtraDimension( thisGalleryElement, 'padding', 'horizontal' );
+ var paddingHeight = self.getExtraDimension( thisGalleryElement, 'padding', 'vertical' );
+
+ var borderWidth = self.getExtraDimension( thisGalleryElement, 'border', 'horizontal' );
+ var borderHeight = self.getExtraDimension( thisGalleryElement, 'border', 'vertical' );
+
+ // Take all outer dimensions into account when resizing so that images
+ // scale with constant empty space between them
+ var outerWidth =
+ thisGalleryElement.data( 'original-width' ) + paddingWidth + borderWidth + marginWidth;
+ var outerHeight =
+ thisGalleryElement.data( 'original-height' ) + paddingHeight + borderHeight + marginHeight;
+
+ // Subtract margins so that images don't overflow on small browser windows
+ thisGalleryElement
+ .width( Math.floor( resizeRatio * outerWidth ) - marginWidth )
+ .height( Math.floor( resizeRatio * outerHeight ) - marginHeight );
+ } );
+ };
+
+ /**
+ * Resizing logic
+ */
+
+ var requestAnimationFrame =
+ window.requestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.msRequestAnimationFrame;
+
+ function attachResizeInAnimationFrames( tiledGalleries ) {
+ var resizing = false;
+ var resizeTimeout = null;
+
+ function handleFrame() {
+ tiledGalleries.resizeAll();
+ if ( resizing ) {
+ requestAnimationFrame( handleFrame );
+ }
+ }
+
+ $( window ).resize( function() {
+ clearTimeout( resizeTimeout );
+
+ if ( ! resizing ) {
+ requestAnimationFrame( handleFrame );
+ }
+ resizing = true;
+ resizeTimeout = setTimeout( function() {
+ resizing = false;
+ }, 15 );
+ } );
+ }
+
+ function attachPlainResize( tiledGalleries ) {
+ $( window ).resize( function() {
+ tiledGalleries.resizeAll();
+ } );
+ }
+
+ /**
+ * Ready, set...
+ */
+
+ $( document ).ready( function() {
+ var tiledGalleries = new TiledGalleryCollection();
+
+ $( 'body' ).on( 'post-load', function( e, maybeResize ) {
+ if ( 'string' === typeof maybeResize && 'resize' === maybeResize ) {
+ tiledGalleries.resizeAll();
+ } else {
+ tiledGalleries.findAndSetupNewGalleries();
+ }
+ } );
+ $( document ).on( 'page-rendered.wpcom-newdash', function() {
+ tiledGalleries.findAndSetupNewGalleries();
+ } );
+
+ // Chrome is a unique snow flake and will start lagging on occasion
+ // It helps if we only resize on animation frames
+ //
+ // For other browsers it seems like there is no lag even if we resize every
+ // time there is an event
+ if ( window.chrome && requestAnimationFrame ) {
+ attachResizeInAnimationFrames( tiledGalleries );
+ } else {
+ attachPlainResize( tiledGalleries );
+ }
+
+ if ( 'undefined' !== typeof wp && wp.customize && wp.customize.selectiveRefresh ) {
+ wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
+ if ( wp.isJetpackWidgetPlaced( placement, 'gallery' ) ) {
+ tiledGalleries.findAndSetupNewGalleries();
+ }
+ } );
+ }
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/tonesque.php b/plugins/jetpack/modules/tonesque.php
new file mode 100644
index 00000000..88c13090
--- /dev/null
+++ b/plugins/jetpack/modules/tonesque.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer needed.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/vaultpress.php b/plugins/jetpack/modules/vaultpress.php
new file mode 100644
index 00000000..6cb93b22
--- /dev/null
+++ b/plugins/jetpack/modules/vaultpress.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Module Name: Backups and Scanning
+ * Module Description: Protect your site with daily or real-time backups and automated virus scanning and threat detection.
+ * First Introduced: 0:1.2
+ * Sort Order: 32
+ * Deactivate: false
+ * Free: false
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Feature: Security, Health
+ * Additional Search Queries: backup, cloud backup, database backup, restore, wordpress backup, backup plugin, wordpress backup plugin, back up, backup wordpress, backwpup, vaultpress, backups, off-site backups, offsite backup, offsite, off-site, antivirus, malware scanner, security, virus, viruses, prevent viruses, scan, anti-virus, antimalware, protection, safe browsing, malware, wp security, wordpress security
+ * Plans: personal, business, premium
+ */
+
+add_action( 'jetpack_modules_loaded', 'vaultpress_jetpack_stub' );
+
+function vaultpress_jetpack_stub() {
+ if ( class_exists( 'VaultPress' ) || function_exists( 'vaultpress_contact_service' ) ) {
+ Jetpack::enable_module_configurable( __FILE__ );
+ add_filter( 'jetpack_module_configuration_url_vaultpress', 'vaultpress_jetpack_configure_url' );
+ add_filter( 'jetpack_module_free_text_vaultpress', 'vaultpress_jetpack_module_free_text' );
+ }
+}
+
+function vaultpress_jetpack_module_free_text() {
+ return __( 'Active', 'jetpack' );
+}
+
+function vaultpress_jetpack_configure_url() {
+ include_once( ABSPATH . '/wp-admin/includes/plugin.php' );
+ return menu_page_url( 'vaultpress', false );
+}
diff --git a/plugins/jetpack/modules/verification-tools.php b/plugins/jetpack/modules/verification-tools.php
new file mode 100644
index 00000000..f99ce68a
--- /dev/null
+++ b/plugins/jetpack/modules/verification-tools.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Module Name: Site verification
+ * Module Description: Establish your site's authenticity with external services.
+ * First Introduced: 3.0
+ * Sort Order: 33
+ * Requires Connection: No
+ * Auto Activate: Yes
+ * Feature: Engagement
+ * Additional Search Queries: webmaster, seo, google, bing, pinterest, search, console
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Load Verification Tools code.
+ */
+function jetpack_load_verification_tools() {
+ include dirname( __FILE__ ) . '/verification-tools/blog-verification-tools.php';
+}
+
+/**
+ * Functionality to load for Verification Tools after all modules have been loaded.
+ */
+function jetpack_verification_tools_loaded() {
+ Jetpack::enable_module_configurable( __FILE__ );
+}
+add_action( 'jetpack_modules_loaded', 'jetpack_verification_tools_loaded' );
+
+jetpack_load_verification_tools();
diff --git a/plugins/jetpack/modules/verification-tools/blog-verification-tools.php b/plugins/jetpack/modules/verification-tools/blog-verification-tools.php
new file mode 100644
index 00000000..593e80c5
--- /dev/null
+++ b/plugins/jetpack/modules/verification-tools/blog-verification-tools.php
@@ -0,0 +1,84 @@
+<?php
+
+// Edit here to add new services
+function jetpack_verification_services() {
+ return array(
+ 'google' => array(
+ 'name' => 'Google Search Console',
+ 'key' => 'google-site-verification',
+ 'format' => 'dBw5CvburAxi537Rp9qi5uG2174Vb6JwHwIRwPSLIK8',
+ 'url' => 'https://www.google.com/webmasters/tools/',
+ ),
+ 'bing' => array(
+ 'name' => 'Bing Webmaster Center',
+ 'key' => 'msvalidate.01',
+ 'format' => '12C1203B5086AECE94EB3A3D9830B2E',
+ 'url' => 'http://www.bing.com/webmaster/',
+ ),
+ 'pinterest' => array(
+ 'name' => 'Pinterest Site Verification',
+ 'key' => 'p:domain_verify',
+ 'format' => 'f100679e6048d45e4a0b0b92dce1efce',
+ 'url' => 'https://pinterest.com/website/verify/',
+ ),
+ 'yandex' => array(
+ 'name' => 'Yandex.Webmaster',
+ 'key' => 'yandex-verification',
+ 'format' => '44d68e1216009f40',
+ 'url' => 'https://webmaster.yandex.com/sites/',
+ ),
+ );
+}
+
+function jetpack_verification_options_init() {
+ register_setting(
+ 'verification_services_codes_fields',
+ 'verification_services_codes',
+ array( 'sanitize_callback' => 'jetpack_verification_validate' )
+ );
+}
+add_action( 'admin_init', 'jetpack_verification_options_init' );
+add_action( 'rest_api_init', 'jetpack_verification_options_init' );
+
+function jetpack_verification_print_meta() {
+ $verification_services_codes = Jetpack_Options::get_option_and_ensure_autoload( 'verification_services_codes', '0' );
+ if ( is_array( $verification_services_codes ) ) {
+ $ver_output = "<!-- Jetpack Site Verification Tags -->\n";
+ foreach ( jetpack_verification_services() as $name => $service ) {
+ if ( is_array( $service ) && ! empty( $verification_services_codes[ "$name" ] ) ) {
+ if ( preg_match( '#^<meta name="([a-z0-9_\-.:]+)?" content="([a-z0-9_-]+)?" />$#i', $verification_services_codes[ "$name" ], $matches ) ) {
+ $verification_code = $matches[2];
+ } else {
+ $verification_code = $verification_services_codes[ "$name" ];
+ }
+ $ver_tag = sprintf( '<meta name="%s" content="%s" />', esc_attr( $service['key'] ), esc_attr( $verification_code ) );
+ /**
+ * Filter the meta tag template used for all verification tools.
+ *
+ * @module verification-tools
+ *
+ * @since 3.0.0
+ *
+ * @param string $ver_tag Verification Tool meta tag.
+ */
+ $ver_output .= apply_filters( 'jetpack_site_verification_output', $ver_tag );
+ $ver_output .= "\n";
+ }
+ }
+ echo $ver_output;
+ }
+}
+add_action( 'wp_head', 'jetpack_verification_print_meta', 1 );
+
+function jetpack_verification_tool_box() {
+ ?>
+ <div class="jp-verification-tools card">
+ <h3 class="title"><?php _e( 'Website Verification Services', 'jetpack' ); ?>&nbsp;<a href="https://jetpack.com/support/site-verification-tools/" rel="noopener noreferrer" target="_blank">(?)</a></h3>
+ <p>
+ <?php printf( __( 'You can verify your site using the <a href="%s">"Site verification" tool in Jetpack Settings</a>.', 'jetpack' ), esc_url( admin_url( 'admin.php?page=jetpack#/traffic' ) ) ); ?>
+ </p>
+ </div>
+ <?php
+}
+
+add_action( 'tool_box', 'jetpack_verification_tool_box', 25 );
diff --git a/plugins/jetpack/modules/verification-tools/verification-tools-utils.php b/plugins/jetpack/modules/verification-tools/verification-tools-utils.php
new file mode 100644
index 00000000..6faec481
--- /dev/null
+++ b/plugins/jetpack/modules/verification-tools/verification-tools-utils.php
@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * Helper functions that are called from API even when module is inactive should be added here.
+ * This file will be included in module-extras.php.
+ */
+
+if ( ! function_exists( 'jetpack_verification_validate' ) ) {
+ function jetpack_verification_validate( $verification_services_codes ) {
+ foreach ( $verification_services_codes as $key => $code ) {
+ // Parse html meta tag if it does not look like a valid code
+ if ( ! preg_match( '/^[a-z0-9_-]+$/i', $code ) ) {
+ $code = jetpack_verification_get_code( $code );
+ }
+
+ $code = esc_attr( trim( $code ) );
+
+ // limit length to 100 chars.
+ $code = substr( $code, 0, 100 );
+
+ /**
+ * Fire after each Verification code was validated.
+ *
+ * @module verification-tools
+ *
+ * @since 3.0.0
+ *
+ * @param string $key Verification service name.
+ * @param string $code Verification service code provided in field in the Tools menu.
+ */
+ do_action( 'jetpack_site_verification_validate', $key, $code );
+
+ $verification_services_codes[ $key ] = $code;
+ }
+ return $verification_services_codes;
+ }
+}
+
+if ( ! function_exists( 'jetpack_verification_get_code' ) ) {
+ function jetpack_verification_get_code( $code ) {
+ $pattern = '/content=["\']?([^"\' ]*)["\' ]/is';
+ preg_match( $pattern, $code, $match );
+ if ( $match ) {
+ return urldecode( $match[1] );
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/plugins/jetpack/modules/videopress.php b/plugins/jetpack/modules/videopress.php
new file mode 100644
index 00000000..8d272b02
--- /dev/null
+++ b/plugins/jetpack/modules/videopress.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Module Name: VideoPress
+ * Module Description: Save on hosting storage and bandwidth costs by streaming fast, ad-free video from our global network.
+ * First Introduced: 2.5
+ * Free: false
+ * Requires Connection: Yes
+ * Sort Order: 27
+ * Module Tags: Photos and Videos
+ * Feature: Writing
+ * Additional Search Queries: video, videos, videopress, video gallery, video player, videoplayer, mobile video, vimeo, youtube, html5 video, stream
+ * Plans: business, premium
+ */
+
+include_once dirname( __FILE__ ) . '/videopress/shortcode.php';
+include_once dirname( __FILE__ ) . '/videopress/class.videopress-options.php';
+include_once dirname( __FILE__ ) . '/videopress/class.videopress-scheduler.php';
+include_once dirname( __FILE__ ) . '/videopress/class.videopress-xmlrpc.php';
+include_once dirname( __FILE__ ) . '/videopress/class.videopress-cli.php';
+include_once dirname( __FILE__ ) . '/videopress/class.jetpack-videopress.php';
+
+if ( is_admin() ) {
+ include_once dirname( __FILE__ ) . '/videopress/editor-media-view.php';
+ include_once dirname( __FILE__ ) . '/videopress/class.videopress-edit-attachment.php';
+ include_once dirname( __FILE__ ) . '/videopress/class.videopress-ajax.php';
+}
diff --git a/plugins/jetpack/modules/videopress/class.jetpack-videopress.php b/plugins/jetpack/modules/videopress/class.jetpack-videopress.php
new file mode 100644
index 00000000..a1073f05
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/class.jetpack-videopress.php
@@ -0,0 +1,340 @@
+<?php
+
+/**
+ * VideoPress in Jetpack
+ */
+class Jetpack_VideoPress {
+ /** @var string */
+ public $module = 'videopress';
+
+ /** @var int */
+ public $version = 5;
+
+ /**
+ * Singleton
+ */
+ public static function init() {
+ static $instance = false;
+
+ if ( ! $instance ) {
+ $instance = new Jetpack_VideoPress();
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Jetpack_VideoPress constructor.
+ *
+ * Sets up the initializer and makes sure that videopress activates and deactivates properly.
+ */
+ private function __construct() {
+ // $this->version = time(); // <s>ghost</s> cache busters!
+ add_action( 'init', array( $this, 'on_init' ) );
+ add_action( 'jetpack_deactivate_module_videopress', array( $this, 'jetpack_module_deactivated' ) );
+ }
+
+ /**
+ * Fires on init
+ */
+ public function on_init() {
+ add_action( 'wp_enqueue_media', array( $this, 'enqueue_admin_scripts' ) );
+ add_filter( 'plupload_default_settings', array( $this, 'videopress_pluploder_config' ) );
+ add_filter( 'wp_get_attachment_url', array( $this, 'update_attachment_url_for_videopress' ), 10, 2 );
+
+ if ( Jetpack_Plan::supports( 'videopress' ) ) {
+ add_filter( 'upload_mimes', array( $this, 'add_video_upload_mimes' ), 999 );
+ }
+
+ add_action( 'admin_print_footer_scripts', array( $this, 'print_in_footer_open_media_add_new' ) );
+ add_action( 'admin_head', array( $this, 'enqueue_admin_styles' ) );
+
+ add_filter( 'wp_mime_type_icon', array( $this, 'wp_mime_type_icon' ), 10, 3 );
+
+ add_filter( 'wp_video_extensions', array( $this, 'add_videopress_extenstion' ) );
+
+ VideoPress_Scheduler::init();
+ VideoPress_XMLRPC::init();
+ }
+
+ /**
+ * Runs when the VideoPress module is deactivated.
+ */
+ public function jetpack_module_deactivated() {
+ VideoPress_Options::delete_options();
+ }
+
+ /**
+ * A can of coke
+ *
+ * Similar to current_user_can, but internal to VideoPress. Returns
+ * true if the given VideoPress capability is allowed by the given user.
+ */
+ public function can( $cap, $user_id = false ) {
+ if ( ! $user_id ) {
+ $user_id = get_current_user_id();
+ }
+
+ // Connection owners are allowed to do all the things.
+ if ( $this->is_connection_owner( $user_id ) ) {
+ return true;
+ }
+
+ // Additional and internal caps checks
+ if ( ! user_can( $user_id, 'upload_files' ) ) {
+ return false;
+ }
+
+ if ( 'edit_videos' == $cap && ! user_can( $user_id, 'edit_others_posts' ) ) {
+ return false;
+ }
+
+ if ( 'delete_videos' == $cap && ! user_can( $user_id, 'delete_others_posts' ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if the provided user is the Jetpack connection owner.
+ */
+ public function is_connection_owner( $user_id = false ) {
+ if ( ! $user_id ) {
+ $user_id = get_current_user_id();
+ }
+
+ $user_token = Jetpack_Data::get_access_token( JETPACK_MASTER_USER );
+
+ return $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) && $user_id === $user_token->external_user_id;
+ }
+
+ /**
+ * Register and enqueue VideoPress admin styles.
+ */
+ public function enqueue_admin_styles() {
+ wp_register_style( 'videopress-admin', plugins_url( 'videopress-admin.css', __FILE__ ), array(), $this->version );
+ wp_enqueue_style( 'videopress-admin' );
+ }
+
+ /**
+ * Register VideoPress admin scripts.
+ */
+ public function enqueue_admin_scripts() {
+ if ( did_action( 'videopress_enqueue_admin_scripts' ) ) {
+ return;
+ }
+
+ if ( $this->should_override_media_uploader() ) {
+ wp_enqueue_script(
+ 'videopress-plupload',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/videopress/js/videopress-plupload.min.js',
+ 'modules/videopress/js/videopress-plupload.js'
+ ),
+ array(
+ 'jquery',
+ 'wp-plupload',
+ ),
+ $this->version
+ );
+
+ wp_enqueue_script(
+ 'videopress-uploader',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/videopress/js/videopress-uploader.min.js',
+ 'modules/videopress/js/videopress-uploader.js'
+ ),
+ array(
+ 'videopress-plupload',
+ ),
+ $this->version
+ );
+
+ wp_enqueue_script(
+ 'media-video-widget-extensions',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/videopress/js/media-video-widget-extensions.min.js',
+ 'modules/videopress/js/media-video-widget-extensions.js'
+ ),
+ array(),
+ $this->version,
+ true
+ );
+ }
+
+ /**
+ * Fires after VideoPress scripts are enqueued in the dashboard.
+ *
+ * @since 2.5.0
+ */
+ do_action( 'videopress_enqueue_admin_scripts' );
+ }
+
+ /**
+ * An override for the attachment url, which returns back the WPCOM VideoPress processed url.
+ *
+ * This is an action proxy to the videopress_get_attachment_url() utility function.
+ *
+ * @param string $url
+ * @param int $post_id
+ *
+ * @return string
+ */
+ public function update_attachment_url_for_videopress( $url, $post_id ) {
+ if ( $videopress_url = videopress_get_attachment_url( $post_id ) ) {
+ return $videopress_url;
+ }
+
+ return $url;
+ }
+
+ /**
+ * Modify the default plupload config to turn on videopress specific filters.
+ */
+ public function videopress_pluploder_config( $config ) {
+
+ if ( ! isset( $config['filters']['max_file_size'] ) ) {
+ $config['filters']['max_file_size'] = wp_max_upload_size() . 'b';
+ }
+
+ $config['filters']['videopress_check_uploads'] = $config['filters']['max_file_size'];
+
+ // We're doing our own check in the videopress_check_uploads filter.
+ unset( $config['filters']['max_file_size'] );
+
+ return $config;
+ }
+
+
+ /**
+ * Helper function to determine if the media uploader should be overridden.
+ *
+ * The rules are simple, only try to load the script when on the edit post or new post pages.
+ *
+ * @return bool
+ */
+ protected function should_override_media_uploader() {
+ global $pagenow;
+
+ // Only load in the admin
+ if ( ! is_admin() ) {
+ return false;
+ }
+
+ $acceptable_pages = array(
+ 'post-new.php',
+ 'post.php',
+ 'upload.php',
+ 'customize.php',
+ );
+
+ // Only load on the post, new post, or upload pages.
+ if ( ! in_array( $pagenow, $acceptable_pages ) ) {
+ return false;
+ }
+
+ $options = VideoPress_Options::get_options();
+
+ return $options['shadow_blog_id'] > 0;
+ }
+
+ /**
+ * A work-around / hack to make it possible to go to the media library with the add new box open.
+ *
+ * @return bool
+ */
+ public function print_in_footer_open_media_add_new() {
+ global $pagenow;
+
+ // Only load in the admin
+ if ( ! is_admin() ) {
+ return false;
+ }
+
+ if ( $pagenow !== 'upload.php' ) {
+ return false;
+ }
+
+ if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'add-new' ) {
+ return false;
+ }
+
+ ?>
+ <script type="text/javascript">
+ ( function( $ ) {
+ window.setTimeout( function() {
+ $('#wp-media-grid .page-title-action').click();
+ }, 500 );
+
+ }( jQuery ) );
+ </script>
+ <?php
+ }
+
+ /**
+ * Makes sure that all video mimes are added in, as multi site installs can remove them.
+ *
+ * @param array $existing_mimes
+ * @return array
+ */
+ public function add_video_upload_mimes( $existing_mimes = array() ) {
+ $mime_types = wp_get_mime_types();
+ $video_types = array_filter( $mime_types, array( $this, 'filter_video_mimes' ) );
+
+ foreach ( $video_types as $key => $value ) {
+ $existing_mimes[ $key ] = $value;
+ }
+
+ // Make sure that videopress mimes are considered videos.
+ $existing_mimes['videopress'] = 'video/videopress';
+
+ return $existing_mimes;
+ }
+
+ /**
+ * Filter designed to get rid of non video mime types.
+ *
+ * @param string $value
+ * @return int
+ */
+ public function filter_video_mimes( $value ) {
+ return preg_match( '@^video/@', $value );
+ }
+
+ /**
+ * @param string $icon
+ * @param string $mime
+ * @param int $post_id
+ *
+ * @return string
+ */
+ public function wp_mime_type_icon( $icon, $mime, $post_id ) {
+
+ if ( $mime !== 'video/videopress' ) {
+ return $icon;
+ }
+
+ $status = get_post_meta( $post_id, 'videopress_status', true );
+
+ if ( $status === 'complete' ) {
+ return $icon;
+ }
+
+ return 'https://wordpress.com/wp-content/mu-plugins/videopress/images/media-video-processing-icon.png';
+ }
+
+ /**
+ * @param array $extensions
+ *
+ * @return array
+ */
+ public function add_videopress_extenstion( $extensions ) {
+ $extensions[] = 'videopress';
+
+ return $extensions;
+ }
+}
+
+// Initialize the module.
+Jetpack_VideoPress::init();
diff --git a/plugins/jetpack/modules/videopress/class.videopress-ajax.php b/plugins/jetpack/modules/videopress/class.videopress-ajax.php
new file mode 100644
index 00000000..e1943c0b
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/class.videopress-ajax.php
@@ -0,0 +1,103 @@
+<?php
+
+class VideoPress_AJAX {
+
+ /**
+ * @var VideoPress_AJAX
+ **/
+ private static $instance = null;
+
+ /**
+ * Private VideoPress_AJAX constructor.
+ *
+ * Use the VideoPress_AJAX::init() method to get an instance.
+ */
+ private function __construct() {
+ add_action( 'wp_ajax_videopress-get-upload-token', array( $this, 'wp_ajax_videopress_get_upload_token' ) );
+
+ add_action(
+ 'wp_ajax_videopress-update-transcoding-status',
+ array(
+ $this,
+ 'wp_ajax_update_transcoding_status',
+ ),
+ -1
+ );
+ }
+
+ /**
+ * Initialize the VideoPress_AJAX and get back a singleton instance.
+ *
+ * @return VideoPress_AJAX
+ */
+ public static function init() {
+ if ( is_null( self::$instance ) ) {
+ self::$instance = new VideoPress_AJAX();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Ajax method that is used by the VideoPress uploader to get a token to upload a file to the wpcom api.
+ *
+ * @return void
+ */
+ public function wp_ajax_videopress_get_upload_token() {
+
+ $options = VideoPress_Options::get_options();
+
+ $args = array(
+ 'method' => 'POST',
+ // 'sslverify' => false,
+ );
+
+ $endpoint = "sites/{$options['shadow_blog_id']}/media/token";
+ $result = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $args );
+
+ if ( is_wp_error( $result ) ) {
+ wp_send_json_error( array( 'message' => __( 'Could not obtain a VideoPress upload token. Please try again later.', 'jetpack' ) ) );
+ return;
+ }
+
+ $response = json_decode( $result['body'], true );
+
+ if ( empty( $response['upload_token'] ) ) {
+ wp_send_json_error( array( 'message' => __( 'Could not obtain a VideoPress upload token. Please try again later.', 'jetpack' ) ) );
+ return;
+ }
+
+ $response['upload_action_url'] = videopress_make_media_upload_path( $options['shadow_blog_id'] );
+
+ wp_send_json_success( $response );
+ }
+
+ /**
+ * Ajax action to update the video transcoding status from the WPCOM API.
+ *
+ * @return void
+ */
+ public function wp_ajax_update_transcoding_status() {
+ if ( ! isset( $_POST['post_id'] ) ) {
+ wp_send_json_error( array( 'message' => __( 'A valid post_id is required.', 'jetpack' ) ) );
+ return;
+ }
+
+ $post_id = (int) $_POST['post_id'];
+
+ if ( ! videopress_update_meta_data( $post_id ) ) {
+ wp_send_json_error( array( 'message' => __( 'That post does not have a VideoPress video associated to it.', 'jetpack' ) ) );
+ return;
+ }
+
+ wp_send_json_success(
+ array(
+ 'message' => __( 'Status updated', 'jetpack' ),
+ 'status' => videopress_get_transcoding_status( $post_id ),
+ )
+ );
+ }
+}
+
+// Let's start this thing up.
+VideoPress_AJAX::init();
diff --git a/plugins/jetpack/modules/videopress/class.videopress-cli.php b/plugins/jetpack/modules/videopress/class.videopress-cli.php
new file mode 100644
index 00000000..e1200da0
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/class.videopress-cli.php
@@ -0,0 +1,167 @@
+<?php
+
+if ( defined( 'WP_CLI' ) && WP_CLI ) {
+
+ /**
+ * VideoPress command line utilities.
+ */
+ class VideoPress_CLI extends WP_CLI_Command {
+ /**
+ * Import a VideoPress Video
+ *
+ * ## OPTIONS
+ *
+ * <guid>: Import the video with the specified guid
+ *
+ * ## EXAMPLES
+ *
+ * wp videopress import kUJmAcSf
+ */
+ public function import( $args ) {
+ $guid = $args[0];
+ $attachment_id = create_local_media_library_for_videopress_guid( $guid );
+ if ( $attachment_id && ! is_wp_error( $attachment_id ) ) {
+ WP_CLI::success( sprintf( __( 'The video has been imported as Attachment ID %d', 'jetpack' ), $attachment_id ) );
+ } else {
+ WP_CLI::error( __( 'An error has been encountered.', 'jetpack' ) );
+ }
+ }
+
+ /**
+ * Manually runs the job to cleanup videos from the media library that failed during the upload process.
+ *
+ * ## EXAMPLES
+ *
+ * wp videopress cleanup_videos
+ */
+ public function cleanup_videos() {
+ $num_cleaned = videopress_cleanup_media_library();
+
+ WP_CLI::success( sprintf( _n( 'Cleaned up %d video.', 'Cleaned up a total of %d videos.', $num_cleaned, 'jetpack' ), $num_cleaned ) );
+ }
+
+ /**
+ * List out all of the crons that can be run.
+ *
+ * ## EXAMPLES
+ *
+ * wp videopress list_crons
+ */
+ public function list_crons() {
+
+ $scheduler = VideoPress_Scheduler::init();
+ $crons = $scheduler->get_crons();
+
+ $schedules = wp_get_schedules();
+
+ if ( count( $crons ) === 0 ) {
+ WP_CLI::success( __( 'Found no available cron jobs.', 'jetpack' ) );
+
+ } else {
+ WP_CLI::success( sprintf( _n( 'Found %d available cron job.', 'Found %d available cron jobs.', count( $crons ), 'jetpack' ), count( $crons ) ) );
+ }
+
+ foreach ( $crons as $cron_name => $cron ) {
+ $interval = isset( $schedules[ $cron['interval'] ]['display'] ) ? $schedules[ $cron['interval'] ]['display'] : $cron['interval'];
+ $runs_next = $scheduler->check_cron( $cron_name );
+ $status = $runs_next ? sprintf( 'Scheduled - Runs Next at %s GMT', gmdate( 'Y-m-d H:i:s', $runs_next ) ) : 'Not Scheduled';
+
+ WP_CLI::log( 'Name: ' . $cron_name );
+ WP_CLI::log( 'Method: ' . $cron['method'] );
+ WP_CLI::log( 'Interval: ' . $interval );
+ WP_CLI::log( 'Status: ' . $status );
+ }
+ }
+
+ /**
+ * Checks for the current status of a cron job.
+ *
+ * ## OPTIONS
+ *
+ * <cron_name>: The name of the cron job to check
+ *
+ * ## EXAMPLES
+ *
+ * wp videopress cron_status cleanup
+ */
+ public function cron_status( $args ) {
+
+ if ( ! isset( $args[0] ) ) {
+ return WP_CLI::error( __( 'You need to provide the name of the cronjob to schedule.', 'jetpack' ) );
+ }
+
+ $scheduler = VideoPress_Scheduler::init();
+
+ if ( ! $scheduler->is_cron_valid( $args[0] ) ) {
+ return WP_CLI::error( sprintf( __( 'There is no cron named %s.', 'jetpack' ), $args[0] ) );
+ }
+
+ $time = $scheduler->check_cron( $args[0] );
+
+ if ( ! $time ) {
+ WP_CLI::success( __( 'The cron is not scheduled to run.', 'jetpack' ) );
+
+ } else {
+ WP_CLI::success( sprintf( __( 'Cron will run at: %s GMT', 'jetpack' ), gmdate( 'Y-m-d H:i:s', $time ) ) );
+ }
+ }
+
+ /**
+ * Actives the given cron job
+ *
+ * ## OPTIONS
+ *
+ * <cron_name>: The name of the cron job to check
+ *
+ * ## EXAMPLES
+ *
+ * wp videopress activate_cron cleanup
+ */
+ public function activate_cron( $args ) {
+
+ if ( ! isset( $args[0] ) ) {
+ WP_CLI::error( __( 'You need to provide the name of the cronjob to schedule.', 'jetpack' ) );
+ }
+
+ $scheduler = VideoPress_Scheduler::init();
+
+ if ( ! $scheduler->is_cron_valid( $args[0] ) ) {
+ return WP_CLI::error( sprintf( __( 'There is no cron named %s.', 'jetpack' ), $args[0] ) );
+ }
+
+ $scheduler->activate_cron( $args[0] );
+
+ WP_CLI::success( sprintf( __( 'The cron named `%s` was scheduled.', 'jetpack' ), $args[0] ) );
+ }
+
+ /**
+ * Actives the given cron job
+ *
+ * ## OPTIONS
+ *
+ * <cron_name>: The name of the cron job to check
+ *
+ * ## EXAMPLES
+ *
+ * wp videopress deactivate_cron cleanup
+ */
+ public function deactivate_cron( $args ) {
+
+ if ( ! isset( $args[0] ) ) {
+ WP_CLI::error( __( 'You need to provide the name of the cronjob to schedule.', 'jetpack' ) );
+ }
+
+ $scheduler = VideoPress_Scheduler::init();
+
+ if ( ! $scheduler->is_cron_valid( $args[0] ) ) {
+ return WP_CLI::error( sprintf( __( 'There is no cron named %s.', 'jetpack' ), $args[0] ) );
+ }
+
+ $scheduler->deactivate_cron( $args[0] );
+
+ WP_CLI::success( sprintf( __( 'The cron named `%s` was removed from the schedule.', 'jetpack' ), $args[0] ) );
+ }
+ }
+
+ WP_CLI::add_command( 'videopress', 'VideoPress_CLI' );
+}
diff --git a/plugins/jetpack/modules/videopress/class.videopress-edit-attachment.php b/plugins/jetpack/modules/videopress/class.videopress-edit-attachment.php
new file mode 100644
index 00000000..be7ddfde
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/class.videopress-edit-attachment.php
@@ -0,0 +1,388 @@
+<?php
+/**
+ * VideoPress edit attachment screen
+ *
+ * @since 4.1
+ */
+class VideoPress_Edit_Attachment {
+
+ /**
+ * Singleton method to initialize the object only once.
+ *
+ * @return VideoPress_Edit_Attachment
+ */
+ public static function init() {
+ static $instance = null;
+
+ if ( ! $instance ) {
+ $instance = new VideoPress_Edit_Attachment();
+ }
+
+ return $instance;
+ }
+
+ /**
+ * VideoPress_Edit_Attachment constructor.
+ *
+ * Adds in appropriate actions for attachment fields editor, meta boxes and saving.
+ */
+ public function __construct() {
+ add_filter( 'attachment_fields_to_edit', array( $this, 'fields_to_edit' ), 10, 2 );
+ add_filter( 'attachment_fields_to_save', array( $this, 'save_fields' ), 10, 2 );
+ add_filter( 'wp_ajax_save-attachment', array( $this, 'save_fields' ), -1 );
+ add_filter( 'wp_ajax_save-attachment-compat', array( $this, 'save_fields' ), -1 );
+
+ add_action( 'add_meta_boxes', array( $this, 'configure_meta_boxes' ), 10, 2 );
+ }
+
+ /**
+ * @param string $post_type
+ * @param object $post
+ */
+ public function configure_meta_boxes( $post_type = 'unknown', $post = null ) {
+ if ( null == $post ) {
+ $post = (object) array( 'ID' => 0 );
+ }
+
+ if ( 'attachment' != $post_type ) {
+ return;
+ }
+
+ // If this has not been processed by videopress, we can skip the rest.
+ if ( ! is_videopress_attachment( $post->ID ) ) {
+ return;
+ }
+
+ add_meta_box( 'videopress-media-info', __( 'VideoPress Information', 'jetpack' ), array( $this, 'videopress_information_box' ), 'attachment', 'side', 'core' );
+ }
+
+ /**
+ * @param array $post
+ * @param array|null $attachment
+ *
+ * @return array
+ */
+ public function save_fields( $post, $attachment = null ) {
+ if ( $attachment === null && isset( $_POST['attachment'] ) ) {
+ $attachment = $_POST['attachment'];
+ }
+
+ if ( ! isset( $attachment['is_videopress_attachment'] ) || $attachment['is_videopress_attachment'] !== 'yes' ) {
+ return $post;
+ }
+
+ $post_id = absint( $post['ID'] );
+
+ $meta = wp_get_attachment_metadata( $post_id );
+
+ // If this has not been processed by videopress, we can skip the rest.
+ if ( ! is_videopress_attachment( $post['ID'] ) ) {
+ return $post;
+ }
+
+ $values = array();
+
+ // Add the video title & description in, so that we save it properly.
+ if ( isset( $_POST['post_title'] ) ) {
+ $values['title'] = trim( strip_tags( $_POST['post_title'] ) );
+ }
+
+ if ( isset( $_POST['post_excerpt'] ) ) {
+ $values['description'] = trim( strip_tags( $_POST['post_excerpt'] ) );
+ }
+
+ if ( isset( $attachment['rating'] ) ) {
+ $rating = $attachment['rating'];
+
+ if ( ! empty( $rating ) && in_array( $rating, array( 'G', 'PG-13', 'R-17', 'X-18' ) ) ) {
+ $values['rating'] = $rating;
+ }
+ }
+
+ // We set a default here, as if it isn't selected, then we'll turn it off.
+ $values['display_embed'] = 0;
+ if ( isset( $attachment['display_embed'] ) ) {
+ $display_embed = $attachment['display_embed'];
+
+ $values['display_embed'] = 'on' === $display_embed ? 1 : 0;
+ }
+
+ $args = array(
+ 'method' => 'POST',
+ );
+
+ $guid = get_post_meta( $post_id, 'videopress_guid', true );
+
+ $endpoint = "videos/{$guid}";
+ $result = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $args, $values );
+
+ if ( is_wp_error( $result ) ) {
+ $post['errors']['videopress']['errors'][] = __( 'There was an issue saving your updates to the VideoPress service. Please try again later.', 'jetpack' );
+ return $post;
+ }
+
+ if ( isset( $values['display_embed'] ) ) {
+ $meta['videopress']['display_embed'] = $values['display_embed'];
+ }
+
+ if ( isset( $values['rating'] ) ) {
+ $meta['videopress']['rating'] = $values['rating'];
+ }
+
+ wp_update_attachment_metadata( $post_id, $meta );
+
+ $response = json_decode( $result['body'], true );
+
+ if ( 'true' !== $response ) {
+ return $post;
+ }
+
+ return $post;
+ }
+
+
+ /**
+ * Get the upload api path.
+ *
+ * @param string $guid
+ * @return string
+ */
+ public function make_video_api_path( $guid ) {
+ return sprintf(
+ '%s://%s/rest/v%s/videos/%s',
+ 'https',
+ 'public-api.wordpress.com', // JETPACK__WPCOM_JSON_API_HOST,
+ Jetpack_Client::WPCOM_JSON_API_VERSION,
+ $guid
+ );
+ }
+
+
+ /**
+ * Creates an array of video fields to edit based on transcoded videos.
+ *
+ * @param array $fields video fields of interest
+ * @param stdClass $post post object
+ * @return array modified version of video fields for administrative interface display
+ */
+ public function fields_to_edit( $fields, $post ) {
+ $post_id = absint( $post->ID );
+
+ $meta = wp_get_attachment_metadata( $post_id );
+
+ // If this has not been processed by videopress, we can skip the rest.
+ if ( ! is_videopress_attachment( $post_id ) || ! isset( $meta['videopress'] ) ) {
+ return $fields;
+ }
+
+ $info = (object) $meta['videopress'];
+ $file_statuses = isset( $meta['file_statuses'] ) ? $meta['file_statuses'] : array();
+
+ $guid = get_post_meta( $post_id, 'videopress_guid', true );
+
+ unset( $fields['url'] );
+ unset( $fields['post_content'] );
+
+ if ( isset( $file_statuses['ogg'] ) && 'done' === $file_statuses['ogg'] ) {
+ $v_name = preg_replace( '/\.\w+/', '', basename( $info->path ) );
+ $video_name = $v_name . '_fmt1.ogv';
+ $ogg_url = videopress_cdn_file_url( $guid, $video_name );
+
+ $fields['video-ogg'] = array(
+ 'label' => __( 'Ogg File URL', 'jetpack' ),
+ 'input' => 'html',
+ 'html' => "<input type='text' class='urlfield' readonly='readonly' name='attachments[$post_id][oggurl]' value='" . esc_url( $ogg_url, array( 'http', 'https' ) ) . "' />",
+ 'helps' => __( 'Location of the Ogg video file.', 'jetpack' ),
+ );
+ }
+
+ $fields['post_title']['helps'] = __( 'Title will appear on the first frame of your video', 'jetpack' );
+
+ $fields['post_excerpt']['label'] = _x( 'Description', 'A header for the short description display', 'jetpack' );
+ $fields['post_excerpt']['input'] = 'textarea';
+ $fields['post_excerpt']['value'] = $info->description;
+
+ $fields['is_videopress_attachment'] = array(
+ 'input' => 'hidden',
+ 'value' => 'yes',
+ );
+
+ $fields['videopress_shortcode'] = array(
+ 'label' => _x( 'Shortcode', 'A header for the shortcode display', 'jetpack' ),
+ 'input' => 'html',
+ 'html' => "<input type=\"text\" name=\"videopress_shortcode\" value=\"[videopress {$guid}]\" readonly=\"readonly\"/>",
+ 'show_in_modal' => true,
+ 'show_in_edit' => false,
+ );
+
+ $fields['display_embed'] = array(
+ 'label' => _x( 'Share', 'A header for the video sharing options area', 'jetpack' ),
+ 'input' => 'html',
+ 'html' => $this->display_embed_choice( $info ),
+ );
+
+ $fields['video-rating'] = array(
+ 'label' => _x( 'Rating', 'A header for the video rating area', 'jetpack' ),
+ 'input' => 'html',
+ 'html' => $this->display_rating( $info ),
+ );
+
+ return $fields;
+ }
+
+ /**
+ * @param stdClass $post
+ */
+ public function videopress_information_box( $post ) {
+ $post_id = absint( $post->ID );
+
+ $meta = wp_get_attachment_metadata( $post_id );
+ $guid = get_post_meta( $post_id, 'videopress_guid', true );
+
+ // If this has not been processed by videopress, we can skip the rest.
+ if ( ! is_videopress_attachment( $post_id ) ) {
+ return;
+ }
+
+ $info = (object) $meta['videopress'];
+
+ $status = videopress_get_transcoding_status( $post_id );
+
+ $formats = array(
+ 'std_mp4' => 'Standard MP4',
+ 'std_ogg' => 'OGG Vorbis',
+ 'dvd_mp4' => 'DVD',
+ 'hd_mp4' => 'High Definition',
+ );
+
+ $embed = "[videopress {$guid}]";
+
+ $shortcode = '<input type="text" id="plugin-embed" readonly="readonly" style="width:180px;" value="' . esc_attr( $embed ) . '" onclick="this.focus();this.select();" />';
+
+ $trans_status = '';
+ $all_trans_done = true;
+ foreach ( $formats as $status_key => $name ) {
+ if ( 'DONE' !== $status[ $status_key ] ) {
+ $all_trans_done = false;
+ }
+
+ $trans_status .= '- <strong>' . $name . ":</strong> <span id=\"status_$status_key\">" . ( 'DONE' === $status[ $status_key ] ? 'Done' : 'Processing' ) . '</span><br>';
+ }
+
+ $nonce = wp_create_nonce( 'videopress-update-transcoding-status' );
+
+ $url = 'empty';
+ if ( ! empty( $guid ) ) {
+ $url = videopress_build_url( $guid );
+ $url = "<a href=\"{$url}\">{$url}</a>";
+ }
+
+ $poster = '<em>Still Processing</em>';
+ if ( ! empty( $info->poster ) ) {
+ $poster = "<br><img src=\"{$info->poster}\" width=\"175px\">";
+ }
+
+ $status_update = '';
+ if ( ! $all_trans_done ) {
+ $status_update = ' (<a href="javascript:;" id="videopress-update-transcoding-status">update</a>)';
+ }
+
+ $html = <<< HTML
+
+<div class="misc-pub-section misc-pub-shortcode">
+ <strong>Shortcode</strong><br>
+ {$shortcode}
+</div>
+<div class="misc-pub-section misc-pub-url">
+ <strong>Url</strong>
+ {$url}
+</div>
+<div class="misc-pub-section misc-pub-poster">
+ <strong>Poster</strong>
+ {$poster}
+</div>
+<div class="misc-pub-section misc-pub-status">
+ <strong>Transcoding Status$status_update:</strong>
+ <div id="videopress-transcoding-status">{$trans_status}</div>
+</div>
+
+
+
+<script>
+ jQuery( function($) {
+ $( '#videopress-update-transcoding-status' ).on( "click", function() {
+ jQuery.ajax( {
+ type: 'post',
+ url: 'admin-ajax.php',
+ data: {
+ action: 'videopress-update-transcoding-status',
+ post_id: '{$post_id}',
+ _ajax_nonce: '{$nonce}'
+ },
+ complete: function( response ) {
+ if ( 200 === response.status ) {
+ var statuses = response.responseJSON.data.status;
+
+ for (var key in statuses) {
+ $('#status_' + key).text( 'DONE' === statuses[key] ? 'Done' : 'Processing' );
+ }
+ }
+ }
+ });
+ } );
+ } );
+</script>
+HTML;
+
+ echo $html;
+ }
+
+ /**
+ * Build HTML to display a form checkbox for embedcode display preference
+ *
+ * @param object $info database row from the videos table
+ * @return string input element of type checkbox set to checked state based on stored embed preference
+ */
+ protected function display_embed_choice( $info ) {
+ $id = "attachments-{$info->post_id}-displayembed";
+ $out = "<label for='$id'><input type='checkbox' name='attachments[{$info->post_id}][display_embed]' id='$id'";
+ if ( $info->display_embed ) {
+ $out .= ' checked="checked"';
+ }
+ $out .= ' />' . __( 'Display share menu and allow viewers to embed or download this video', 'jetpack' ) . '</label>';
+ return $out;
+ }
+
+ /**
+ * Build HTML to display a form input radio button for video ratings
+ *
+ * @param object $info database row from the videos table
+ * @return string input elements of type radio with existing stored value selected
+ */
+ protected function display_rating( $info ) {
+ $out = '';
+
+ $ratings = array(
+ 'G' => 'G',
+ 'PG-13' => 'PG-13',
+ 'R-17' => 'R',
+ 'X-18' => 'X',
+ );
+
+ foreach ( $ratings as $r => $label ) {
+ $id = "attachments-{$info->post_id}-rating-$r";
+ $out .= "<label for=\"$id\"><input type=\"radio\" name=\"attachments[{$info->post_id}][rating]\" id=\"$id\" value=\"$r\"";
+ if ( $info->rating == $r ) {
+ $out .= ' checked="checked"';
+ }
+
+ $out .= " />$label</label>";
+ unset( $id );
+ }
+
+ return $out;
+ }
+}
+
+// Let's start this thing up.
+VideoPress_Edit_Attachment::init();
diff --git a/plugins/jetpack/modules/videopress/class.videopress-gutenberg.php b/plugins/jetpack/modules/videopress/class.videopress-gutenberg.php
new file mode 100644
index 00000000..be0bd1db
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/class.videopress-gutenberg.php
@@ -0,0 +1,169 @@
+<?php
+/**
+ * Block Editor functionality for VideoPress users.
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Register a VideoPress extension to replace the default Core Video block.
+ */
+class VideoPress_Gutenberg {
+
+ /**
+ * Singleton
+ */
+ public static function init() {
+ static $instance = false;
+
+ if ( ! $instance ) {
+ $instance = new VideoPress_Gutenberg();
+ }
+
+ return $instance;
+ }
+
+ /**
+ * VideoPress_Gutenberg constructor.
+ *
+ * Initialize the VideoPress Gutenberg extension
+ */
+ private function __construct() {
+ add_action( 'init', array( $this, 'register_video_block_with_videopress' ) );
+ add_action( 'jetpack_register_gutenberg_extensions', array( $this, 'set_extension_availability' ) );
+ }
+
+ /**
+ * Used to check whether VideoPress is enabled for given site.
+ *
+ * @todo Create a global `jetpack_check_module_availability( $module )` helper so we can re-use it on other modules.
+ * This global helper should be created in a file synced with WordPress.com so we can use it there too.
+ * @see https://github.com/Automattic/jetpack/pull/11321#discussion_r255477815
+ *
+ * @return array Associative array indicating if the module is available (key `available`) and the reason why it is
+ * unavailable (key `unavailable_reason`)
+ */
+ public function check_videopress_availability() {
+ // It is available on Simple Sites having the appropriate a plan.
+ if (
+ defined( 'IS_WPCOM' ) && IS_WPCOM
+ && method_exists( 'Store_Product_List', 'get_site_specific_features_data' )
+ ) {
+ $features = Store_Product_List::get_site_specific_features_data();
+ if ( in_array( 'videopress', $features['active'], true ) ) {
+ return array( 'available' => true );
+ } else {
+ return array(
+ 'available' => false,
+ 'unavailable_reason' => 'missing_plan',
+ );
+ }
+ }
+
+ // It is available on Jetpack Sites having the module active.
+ if (
+ method_exists( 'Jetpack', 'is_active' ) && Jetpack::is_active()
+ && method_exists( 'Jetpack', 'is_module_active' )
+ && method_exists( 'Jetpack_Plan', 'supports' )
+ ) {
+ if ( Jetpack::is_module_active( 'videopress' ) ) {
+ return array( 'available' => true );
+ } elseif ( ! Jetpack_Plan::supports( 'videopress' ) ) {
+ return array(
+ 'available' => false,
+ 'unavailable_reason' => 'missing_plan',
+ );
+ } else {
+ return array(
+ 'available' => false,
+ 'unavailable_reason' => 'missing_module',
+ );
+ }
+ }
+
+ return array(
+ 'available' => false,
+ 'unavailable_reason' => 'unknown',
+ );
+ }
+
+ /**
+ * Set the Jetpack Gutenberg extension availability.
+ */
+ public function set_extension_availability() {
+ $availability = $this->check_videopress_availability();
+ if ( $availability['available'] ) {
+ Jetpack_Gutenberg::set_extension_available( 'jetpack/videopress' );
+ } else {
+ Jetpack_Gutenberg::set_extension_unavailable( 'jetpack/videopress', $availability['unavailable_reason'] );
+ }
+ }
+
+ /**
+ * Register the core video block as a dynamic block.
+ *
+ * It defines a server-side rendering that adds VideoPress support to the core video block.
+ */
+ public function register_video_block_with_videopress() {
+ jetpack_register_block(
+ 'core/video',
+ array(
+ 'render_callback' => array( $this, 'render_video_block_with_videopress' ),
+ )
+ );
+ }
+
+ /**
+ * Render the core video block replacing the src attribute with the VideoPress URL
+ *
+ * @param array $attributes Array containing the video block attributes.
+ * @param string $content String containing the video block content.
+ *
+ * @return string
+ */
+ public function render_video_block_with_videopress( $attributes, $content ) {
+ if ( ! isset( $attributes['id'] ) || isset( $attributes['guid'] ) ) {
+ return $content;
+ }
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $blog_id = get_current_blog_id();
+ } elseif ( method_exists( 'Jetpack', 'is_active' ) && Jetpack::is_active() ) {
+ /**
+ * We're intentionally not using `get_current_blog_id` because it was returning unexpected values.
+ *
+ * @see https://github.com/Automattic/jetpack/pull/11193#issuecomment-457883886
+ * @see https://github.com/Automattic/jetpack/pull/11193/commits/215cf789f3d8bd03ff9eb1bbdb693acb8831d273
+ */
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ }
+
+ if ( ! isset( $blog_id ) ) {
+ return $content;
+ }
+
+ $post_id = absint( $attributes['id'] );
+ $videopress_id = video_get_info_by_blogpostid( $blog_id, $post_id )->guid;
+ $videopress_data = videopress_get_video_details( $videopress_id );
+
+ if ( empty( $videopress_data->file_url_base->https ) || empty( $videopress_data->files->hd->mp4 ) ) {
+ return $content;
+ }
+
+ $videopress_url = $videopress_data->file_url_base->https . $videopress_data->files->hd->mp4;
+
+ $pattern = '/(\s)src=([\'"])(?:(?!\2).)+?\2/';
+
+ return preg_replace(
+ $pattern,
+ sprintf(
+ '\1src="%1$s"',
+ esc_url_raw( $videopress_url )
+ ),
+ $content,
+ 1
+ );
+ }
+}
+
+VideoPress_Gutenberg::init();
diff --git a/plugins/jetpack/modules/videopress/class.videopress-options.php b/plugins/jetpack/modules/videopress/class.videopress-options.php
new file mode 100644
index 00000000..b8049e37
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/class.videopress-options.php
@@ -0,0 +1,59 @@
+<?php
+
+class VideoPress_Options {
+
+ /** @var string */
+ public static $option_name = 'videopress';
+
+ /** @var array */
+ protected static $options = array();
+
+ /**
+ * Get VideoPress options
+ */
+ public static function get_options() {
+ // Make sure we only get options from the database and services once per connection.
+ if ( count( self::$options ) > 0 ) {
+ return self::$options;
+ }
+
+ $defaults = array(
+ 'meta' => array(
+ 'max_upload_size' => 0,
+ ),
+ );
+
+ self::$options = Jetpack_Options::get_option( self::$option_name, array() );
+ self::$options = array_merge( $defaults, self::$options );
+
+ // Make sure that the shadow blog id never comes from the options, but instead uses the
+ // associated shadow blog id, if videopress is enabled.
+ self::$options['shadow_blog_id'] = 0;
+
+ // Use the Jetpack ID for the shadow blog ID if we have a plan that supports VideoPress
+ if ( Jetpack_Plan::supports( 'videopress' ) ) {
+ self::$options['shadow_blog_id'] = Jetpack_Options::get_option( 'id' );
+ }
+
+ return self::$options;
+ }
+
+ /**
+ * Update VideoPress options
+ */
+ public static function update_options( $options ) {
+ Jetpack_Options::update_option( self::$option_name, $options );
+
+ self::$options = $options;
+ }
+
+ /**
+ * Runs when the VideoPress module is deactivated.
+ */
+ public static function delete_options() {
+ Jetpack_Options::delete_option( self::$option_name );
+
+ self::$options = array();
+ }
+
+}
diff --git a/plugins/jetpack/modules/videopress/class.videopress-player.php b/plugins/jetpack/modules/videopress/class.videopress-player.php
new file mode 100644
index 00000000..669523c9
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/class.videopress-player.php
@@ -0,0 +1,880 @@
+<?php
+/**
+ * VideoPress playback module markup generator.
+ *
+ * @since 1.3
+ */
+class VideoPress_Player {
+ /**
+ * Video data for the requested guid and maximum width
+ *
+ * @since 1.3
+ * @var VideoPress_Video
+ */
+ protected $video;
+
+ /**
+ * DOM identifier of the video container
+ *
+ * @var string
+ * @since 1.3
+ */
+ protected $video_container_id;
+
+ /**
+ * DOM identifier of the video element (video, object, embed)
+ *
+ * @var string
+ * @since 1.3
+ */
+ protected $video_id;
+
+ /**
+ * Array of playback options: force_flash or freedom
+ *
+ * @var array
+ * @since 1.3
+ */
+ protected $options;
+
+ /**
+ * Array of video GUIDs shown and their counts,
+ * moved from the old VideoPress class.
+ */
+ public static $shown = array();
+
+ /**
+ * Initiate a player object based on shortcode values and possible blog-level option overrides
+ *
+ * @since 1.3
+ * @var string $guid VideoPress unique identifier
+ * @var int $maxwidth maximum desired width of the video player if specified
+ * @var array $options player customizations
+ */
+ public function __construct( $guid, $maxwidth = 0, $options = array() ) {
+ if ( empty( self::$shown[ $guid ] ) ) {
+ self::$shown[ $guid ] = 0;
+ }
+
+ self::$shown[ $guid ]++;
+
+ $this->video_container_id = 'v-' . $guid . '-' . self::$shown[ $guid ];
+ $this->video_id = $this->video_container_id . '-video';
+
+ if ( is_array( $options ) ) {
+ $this->options = $options;
+ } else {
+ $this->options = array();
+ }
+
+ // set up the video
+ $cache_key = null;
+
+ // disable cache in debug mode
+ if ( defined( 'WP_DEBUG' ) && WP_DEBUG === true ) {
+ $cached_video = null;
+ } else {
+ $cache_key_pieces = array( 'video' );
+
+ if ( is_multisite() && is_subdomain_install() ) {
+ $cache_key_pieces[] = get_current_blog_id();
+ }
+
+ $cache_key_pieces[] = $guid;
+ if ( $maxwidth > 0 ) {
+ $cache_key_pieces[] = $maxwidth;
+ }
+ if ( is_ssl() ) {
+ $cache_key_pieces[] = 'ssl';
+ }
+ $cache_key = implode( '-', $cache_key_pieces );
+ unset( $cache_key_pieces );
+ $cached_video = wp_cache_get( $cache_key, 'video' );
+ }
+ if ( empty( $cached_video ) ) {
+ $video = new VideoPress_Video( $guid, $maxwidth );
+ if ( empty( $video ) ) {
+ return;
+ } elseif ( isset( $video->error ) ) {
+ $this->video = $video->error;
+ return;
+ } elseif ( is_wp_error( $video ) ) {
+ $this->video = $video;
+ return;
+ }
+
+ $this->video = $video;
+ unset( $video );
+
+ if ( ! defined( 'WP_DEBUG' ) || WP_DEBUG !== true ) {
+ $expire = 3600;
+ if ( isset( $video->expires ) && is_int( $video->expires ) ) {
+ $expires_diff = time() - $video->expires;
+ if ( $expires_diff > 0 && $expires_diff < 86400 ) { // allowed range: 1 second to 1 day
+ $expire = $expires_diff;
+ }
+ unset( $expires_diff );
+ }
+
+ wp_cache_set( $cache_key, serialize( $this->video ), 'video', $expire );
+ unset( $expire );
+ }
+ } else {
+ $this->video = unserialize( $cached_video );
+ }
+ unset( $cache_key );
+ unset( $cached_video );
+ }
+
+ /**
+ * Wrap output in a VideoPress player container
+ *
+ * @since 1.3
+ * @var string $content HTML string
+ * @return string HTML string or blank string if nothing to wrap
+ */
+ private function html_wrapper( $content ) {
+ if ( empty( $content ) ) {
+ return '';
+ } else {
+ return '<div id="' . esc_attr( $this->video_container_id ) . '" class="video-player">' . $content . '</div>';
+ }
+ }
+
+ /**
+ * Output content suitable for a feed reader displaying RSS or Atom feeds
+ * We do not display error messages in the feed view due to caching concerns.
+ * Flash content presented using <embed> markup for feed reader compatibility.
+ *
+ * @since 1.3
+ * @return string HTML string or empty string if error
+ */
+ public function asXML() {
+ if ( empty( $this->video ) || is_wp_error( $this->video ) ) {
+ return '';
+ }
+
+ if ( isset( $this->options['force_flash'] ) && true === $this->options['force_flash'] ) {
+ $content = $this->flash_embed();
+
+ } else {
+ $content = $this->html5_static();
+ }
+
+ return $this->html_wrapper( $content );
+ }
+
+ /**
+ * Video player markup for best matching the current request and publisher options
+ *
+ * @since 1.3
+ * @return string HTML markup string or empty string if no video property found
+ */
+ public function asHTML() {
+ if ( empty( $this->video ) ) {
+ $content = '';
+
+ } elseif ( is_wp_error( $this->video ) ) {
+ $content = $this->error_message( $this->video );
+
+ } elseif ( isset( $this->options['force_flash'] ) && true === $this->options['force_flash'] ) {
+ $content = $this->flash_object();
+
+ } elseif ( isset( $this->video->restricted_embed ) && true === $this->video->restricted_embed ) {
+
+ if ( $this->options['forcestatic'] ) {
+ $content = $this->flash_object();
+
+ } else {
+ $content = $this->html5_dynamic();
+ }
+ } elseif ( isset( $this->options['freedom'] ) && true === $this->options['freedom'] ) {
+ $content = $this->html5_static();
+
+ } else {
+ $content = $this->html5_dynamic();
+ }
+
+ return $this->html_wrapper( $content );
+ }
+
+ /**
+ * Display an error message to users capable of doing something about the error
+ *
+ * @since 1.3
+ * @uses current_user_can() to test if current user has edit_posts capability
+ * @var WP_Error $error WordPress error
+ * @return string HTML string
+ */
+ private function error_message( $error ) {
+ if ( ! current_user_can( 'edit_posts' ) || empty( $error ) ) {
+ return '';
+ }
+
+ $html = '<div class="videopress-error" style="background-color:rgb(255,0,0);color:rgb(255,255,255);font-family:font-family:\'Helvetica Neue\',Arial,Helvetica,\'Nimbus Sans L\',sans-serif;font-size:140%;min-height:10em;padding-top:1.5em;padding-bottom:1.5em">';
+ $html .= '<h1 style="font-size:180%;font-style:bold;line-height:130%;text-decoration:underline">' . esc_html( sprintf( __( '%s Error', 'jetpack' ), 'VideoPress' ) ) . '</h1>';
+ foreach ( $error->get_error_messages() as $message ) {
+ $html .= $message;
+ }
+ $html .= '</div>';
+ return $html;
+ }
+
+ /**
+ * Rating agencies and industry associations require a potential viewer verify his or her age before a video or its poster frame are displayed.
+ * Content rated for audiences 17 years of age or older requires such verification across multiple rating agencies and industry associations
+ *
+ * @since 1.3
+ * @return bool true if video requires the viewer verify he or she is 17 years of age or older
+ */
+ private function age_gate_required() {
+ if ( isset( $this->video->age_rating ) && $this->video->age_rating >= 17 ) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Select a date of birth using HTML form elements.
+ *
+ * @since 1.5
+ * @return string HTML markup
+ */
+ private function html_age_gate() {
+ global $wp_locale;
+ $text_align = 'left';
+ if ( $this->video->text_direction === 'rtl' ) {
+ $text_align = 'right';
+ }
+
+ $html = '<div class="videopress-age-gate" style="margin:0 60px">';
+ $html .= '<p class="instructions" style="color:rgb(255, 255, 255);font-size:21px;padding-top:60px;padding-bottom:20px;text-align:' . $text_align . '">' . esc_html( __( 'This video is intended for mature audiences.', 'jetpack' ) ) . '<br />' . esc_html( __( 'Please verify your birthday.', 'jetpack' ) ) . '</p>';
+ $html .= '<fieldset id="birthday" style="border:0 none;text-align:' . $text_align . ';padding:0;">';
+ $inputs_style = 'border:1px solid #444;margin-';
+ if ( $this->video->text_direction === 'rtl' ) {
+ $inputs_style .= 'left';
+ } else {
+ $inputs_style .= 'right';
+ }
+ $inputs_style .= ':10px;background-color:rgb(0, 0, 0);font-size:14px;color:rgb(255,255,255);padding:4px 6px;line-height: 2em;vertical-align: middle';
+
+ /**
+ * Display a list of months in the Gregorian calendar.
+ * Set values to 0-based to match JavaScript Date.
+ *
+ * @link https://developer.mozilla.org/en/JavaScript/Reference/global_objects/date Mozilla JavaScript Reference: Date
+ */
+ $html .= '<select name="month" style="' . $inputs_style . '">';
+
+ for ( $i = 0; $i < 12; $i++ ) {
+ $html .= '<option value="' . esc_attr( $i ) . '">' . esc_html( $wp_locale->get_month( $i + 1 ) ) . '</option>';
+ }
+ $html .= '</select>';
+
+ /**
+ * todo: numdays variance by month
+ */
+ $html .= '<select name="day" style="' . $inputs_style . '">';
+ for ( $i = 1; $i < 32; $i++ ) {
+ $html .= '<option>' . $i . '</option>';
+ }
+ $html .= '</select>';
+
+ /**
+ * Current record for human life is 122. Go back 130 years and no one is left out.
+ * Don't ask infants younger than 2 for their birthday
+ * Default to 13
+ */
+ $html .= '<select name="year" style="' . $inputs_style . '">';
+ $start_year = date( 'Y' ) - 2;
+ $default_year = $start_year - 11;
+ $end_year = $start_year - 128;
+ for ( $year = $start_year; $year > $end_year; $year-- ) {
+ $html .= '<option';
+ if ( $year === $default_year ) {
+ $html .= ' selected="selected"';
+ }
+ $html .= '>' . $year . '</option>';
+ }
+ unset( $start_year );
+ unset( $default_year );
+ unset( $end_year );
+ $html .= '</select>';
+
+ $html .= '<input type="submit" value="' . __( 'Submit', 'jetpack' ) . '" style="cursor:pointer;border-radius: 1em;border:1px solid #333;background-color:#333;background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0, #444), color-stop(1, #111) );background:-moz-linear-gradient(center top, #444 0%, #111 100%);font-size:13px;padding:4px 10px 5px;line-height:1em;vertical-align:top;color:white;text-decoration:none;margin:0" />';
+
+ $html .= '</fieldset>';
+ $html .= '<p style="padding-top:20px;padding-bottom:60px;text-align:' . $text_align . ';"><a rel="nofollow noopener noreferrer" href="http://videopress.com/" target="_blank" style="color:rgb(128,128,128);text-decoration:underline;font-size:15px">' . __( 'More information', 'jetpack' ) . '</a></p>';
+
+ $html .= '</div>';
+ return $html;
+ }
+
+ /**
+ * Return HTML5 video static markup for the given video parameters.
+ * Use default browser player controls.
+ * No Flash fallback.
+ *
+ * @since 1.2
+ * @link http://www.whatwg.org/specs/web-apps/current-work/multipage/video.html HTML5 video
+ * @return string HTML5 video element and children
+ */
+ private function html5_static() {
+ wp_enqueue_script( 'videopress' );
+ $thumbnail = esc_url( $this->video->poster_frame_uri );
+ $html = "<video id=\"{$this->video_id}\" width=\"{$this->video->calculated_width}\" height=\"{$this->video->calculated_height}\" poster=\"$thumbnail\" controls=\"true\"";
+ if ( isset( $this->options['autoplay'] ) && $this->options['autoplay'] === true ) {
+ $html .= ' autoplay="true"';
+ } else {
+ $html .= ' preload="metadata"';
+ }
+ if ( isset( $this->video->text_direction ) ) {
+ $html .= ' dir="' . esc_attr( $this->video->text_direction ) . '"';
+ }
+ if ( isset( $this->video->language ) ) {
+ $html .= ' lang="' . esc_attr( $this->video->language ) . '"';
+ }
+ $html .= '>';
+ if ( ! isset( $this->options['freedom'] ) || $this->options['freedom'] === false ) {
+ $mp4 = $this->video->videos->mp4->url;
+ if ( ! empty( $mp4 ) ) {
+ $html .= '<source src="' . esc_url( $mp4 ) . '" type="video/mp4; codecs=&quot;' . esc_attr( $this->video->videos->mp4->codecs ) . '&quot;" />';
+ }
+ unset( $mp4 );
+ }
+
+ if ( isset( $this->video->videos->ogv ) ) {
+ $ogg = $this->video->videos->ogv->url;
+ if ( ! empty( $ogg ) ) {
+ $html .= '<source src="' . esc_url( $ogg ) . '" type="video/ogg; codecs=&quot;' . esc_attr( $this->video->videos->ogv->codecs ) . '&quot;" />';
+ }
+
+ unset( $ogg );
+ }
+
+ $html .= '<div><img alt="';
+ if ( isset( $this->video->title ) ) {
+ $html .= esc_attr( $this->video->title );
+ }
+ $html .= '" src="' . $thumbnail . '" width="' . $this->video->calculated_width . '" height="' . $this->video->calculated_height . '" /></div>';
+ if ( isset( $this->options['freedom'] ) && $this->options['freedom'] === true ) {
+ $html .= '<p class="robots-nocontent">' . sprintf( __( 'You do not have sufficient <a rel="nofollow noopener noreferrer" href="%s" target="_blank">freedom levels</a> to view this video. Support free software and upgrade.', 'jetpack' ), 'http://www.gnu.org/philosophy/free-sw.html' ) . '</p>';
+ } elseif ( isset( $this->video->title ) ) {
+ $html .= '<p>' . esc_html( $this->video->title ) . '</p>';
+ }
+ $html .= '</video>';
+ return $html;
+ }
+
+ /**
+ * Click to play dynamic HTML5-capable player.
+ * The player displays a video preview section including poster frame,
+ * video title, play button and watermark on the original page load
+ * and calculates the playback capabilities of the browser. The video player
+ * is loaded when the visitor clicks on the video preview area.
+ * If Flash Player 10 or above is available the browser will display
+ * the Flash version of the video. If HTML5 video appears to be supported
+ * and the browser may be capable of MP4 (H.264, AAC) or OGV (Theora, Vorbis)
+ * playback the browser will display its native HTML5 player.
+ *
+ * @since 1.5
+ * @return string HTML markup
+ */
+ private function html5_dynamic() {
+
+ /**
+ * Filter the VideoPress legacy player feature
+ *
+ * This filter allows you to control whether the legacy VideoPress player should be used
+ * instead of the improved one.
+ *
+ * @module videopress
+ *
+ * @since 3.7.0
+ *
+ * @param boolean $videopress_use_legacy_player
+ */
+ if ( ! apply_filters( 'jetpack_videopress_use_legacy_player', false ) ) {
+ return $this->html5_dynamic_next();
+ }
+
+ wp_enqueue_script( 'videopress' );
+ $video_placeholder_id = $this->video_container_id . '-placeholder';
+ $age_gate_required = $this->age_gate_required();
+ $width = absint( $this->video->calculated_width );
+ $height = absint( $this->video->calculated_height );
+
+ $html = '<div id="' . $video_placeholder_id . '" class="videopress-placeholder" style="';
+ if ( $age_gate_required ) {
+ $html .= "min-width:{$width}px;min-height:{$height}px";
+ } else {
+ $html .= "width:{$width}px;height:{$height}px";
+ }
+ $html .= ';display:none;cursor:pointer !important;position:relative;';
+ if ( isset( $this->video->skin ) && isset( $this->video->skin->background_color ) ) {
+ $html .= 'background-color:' . esc_attr( $this->video->skin->background_color ) . ';';
+ }
+ $html .= 'font-family: \'Helvetica Neue\',Arial,Helvetica,\'Nimbus Sans L\',sans-serif;font-weight:bold;font-size:18px">' . PHP_EOL;
+
+ /**
+ * Do not display a poster frame, title, or any other content hints for mature content.
+ */
+ if ( ! $age_gate_required ) {
+ if ( ! empty( $this->video->title ) ) {
+ $html .= '<div class="videopress-title" style="display:inline;position:absolute;margin:20px 20px 0 20px;padding:4px 8px;vertical-align:top;text-align:';
+ if ( $this->video->text_direction === 'rtl' ) {
+ $html .= 'right" dir="rtl"';
+ } else {
+ $html .= 'left" dir="ltr"';
+ }
+ if ( isset( $this->video->language ) ) {
+ $html .= ' lang="' . esc_attr( $this->video->language ) . '"';
+ }
+ $html .= '><span style="padding:3px 0;line-height:1.5em;';
+ if ( isset( $this->video->skin ) && isset( $this->video->skin->background_color ) ) {
+ $html .= 'background-color:';
+ if ( $this->video->skin->background_color === 'rgb(0,0,0)' ) {
+ $html .= 'rgba(0,0,0,0.8)';
+ } else {
+ $html .= esc_attr( $this->video->skin->background_color );
+ }
+ $html .= ';';
+ }
+ $html .= 'color:rgb(255,255,255)">' . esc_html( $this->video->title ) . '</span></div>';
+ }
+ $html .= '<img class="videopress-poster" alt="';
+ if ( ! empty( $this->video->title ) ) {
+ $html .= esc_attr( $this->video->title ) . '" title="' . esc_attr( sprintf( _x( 'Watch: %s', 'watch a video title', 'jetpack' ), $this->video->title ) );
+ }
+ $html .= '" src="' . esc_url( $this->video->poster_frame_uri, array( 'http', 'https' ) ) . '" width="' . $width . '" height="' . $height . '" />' . PHP_EOL;
+
+ // style a play button hovered over the poster frame
+ $html .= '<div class="play-button"><span style="z-index:2;display:block;position:absolute;top:50%;left:50%;text-align:center;vertical-align:middle;color:rgb(255,255,255);opacity:0.9;margin:0 0 0 -0.45em;padding:0;line-height:0;font-size:500%;text-shadow:0 0 40px rgba(0,0,0,0.5)">&#9654;</span></div>' . PHP_EOL;
+
+ // watermark
+ if ( isset( $this->video->skin ) && isset( $this->video->skin->watermark ) ) {
+ $html .= '<div style="position:relative;margin-top:-40px;height:25px;margin-bottom:35px;';
+ if ( $this->video->text_direction === 'rtl' ) {
+ $html .= 'margin-left:20px;text-align:left;';
+ } else {
+ $html .= 'margin-right:20px;text-align:right;';
+ }
+ $html .= 'vertical-align:bottom;z-index:3">';
+ $html .= '<img alt="" src="' . esc_url( $this->video->skin->watermark, array( 'http', 'https' ) ) . '" width="90" height="13" style="background-color:transparent;background-image:none;background-repeat:no-repeat;border:none;margin:0;padding:0"/>';
+ $html .= '</div>' . PHP_EOL;
+ }
+ }
+
+ $data = array(
+ 'blog' => absint( $this->video->blog_id ),
+ 'post' => absint( $this->video->post_id ),
+ 'duration' => absint( $this->video->duration ),
+ 'poster' => esc_url_raw( $this->video->poster_frame_uri, array( 'http', 'https' ) ),
+ 'hd' => (bool) $this->options['hd'],
+ );
+ if ( isset( $this->video->videos ) ) {
+ if ( isset( $this->video->videos->mp4 ) && isset( $this->video->videos->mp4->url ) ) {
+ $data['mp4'] = array(
+ 'size' => $this->video->videos->mp4->format,
+ 'uri' => esc_url_raw( $this->video->videos->mp4->url, array( 'http', 'https' ) ),
+ );
+ }
+ if ( isset( $this->video->videos->ogv ) && isset( $this->video->videos->ogv->url ) ) {
+ $data['ogv'] = array(
+ 'size' => 'std',
+ 'uri' => esc_url_raw( $this->video->videos->ogv->url, array( 'http', 'https' ) ),
+ );
+ }
+ }
+ $locale = array( 'dir' => $this->video->text_direction );
+ if ( isset( $this->video->language ) ) {
+ $locale['lang'] = $this->video->language;
+ }
+ $data['locale'] = $locale;
+ unset( $locale );
+
+ $guid = $this->video->guid;
+ $guid_js = json_encode( $guid );
+ $html .= '<script type="text/javascript">' . PHP_EOL;
+ $html .= 'jQuery(document).ready(function() {';
+
+ $html .= 'if ( !jQuery.VideoPress.data[' . json_encode( $guid ) . '] ) { jQuery.VideoPress.data[' . json_encode( $guid ) . '] = new Array(); }' . PHP_EOL;
+ $html .= 'jQuery.VideoPress.data[' . json_encode( $guid ) . '][' . self::$shown[ $guid ] . ']=' . json_encode( $data ) . ';' . PHP_EOL;
+ unset( $data );
+
+ $jq_container = json_encode( '#' . $this->video_container_id );
+ $jq_placeholder = json_encode( '#' . $video_placeholder_id );
+ $player_config = "{width:{$width},height:{$height},";
+ if ( isset( $this->options['freedom'] ) && $this->options['freedom'] === true ) {
+ $player_config .= 'freedom:"true",';
+ }
+ $player_config .= 'container:jQuery(' . $jq_container . ')}';
+
+ $html .= "jQuery({$jq_placeholder}).show(0,function(){jQuery.VideoPress.analytics.impression({$guid_js})});" . PHP_EOL;
+
+ if ( $age_gate_required ) {
+ $html .= 'if ( jQuery.VideoPress.support.flash() ) {' . PHP_EOL;
+ /**
+ * @link http://code.google.com/p/swfobject/wiki/api#swfobject.embedSWF(swfUrlStr,_replaceElemIdStr,_widthStr,_height
+ */
+ $html .= 'swfobject.embedSWF(' . implode(
+ ',',
+ array(
+ 'jQuery.VideoPress.video.flash.player_uri',
+ json_encode( $this->video_container_id ),
+ json_encode( $width ),
+ json_encode( $height ),
+ 'jQuery.VideoPress.video.flash.min_version',
+ 'jQuery.VideoPress.video.flash.expressinstall', // attempt to upgrade the Flash player if less than min_version. requires a 310x137 container or larger but we will always try to include
+ '{guid:' . $guid_js . '}', // FlashVars
+ 'jQuery.VideoPress.video.flash.params',
+ 'null', // no attributes
+ 'jQuery.VideoPress.video.flash.embedCallback', // error fallback
+ )
+ ) . ');';
+ $html .= '} else {' . PHP_EOL;
+ $html .= "if ( jQuery.VideoPress.video.prepare({$guid_js},{$player_config}," . self::$shown[ $guid ] . ') ) {' . PHP_EOL;
+ $html .= 'if ( jQuery(' . $jq_container . ').data( "player" ) === "flash" ){jQuery.VideoPress.video.play(jQuery(' . json_encode( '#' . $this->video_container_id ) . '));}else{';
+ $html .= 'jQuery(' . $jq_placeholder . ').html(' . json_encode( $this->html_age_date() ) . ');' . PHP_EOL;
+ $html .= 'jQuery(' . json_encode( '#' . $video_placeholder_id . ' input[type=submit]' ) . ').one("click", function(event){jQuery.VideoPress.requirements.isSufficientAge(jQuery(' . $jq_container . '),' . absint( $this->video->age_rating ) . ')});' . PHP_EOL;
+ $html .= '}}}' . PHP_EOL;
+ } else {
+ $html .= "if ( jQuery.VideoPress.video.prepare({$guid_js}, {$player_config}," . self::$shown[ $guid ] . ') ) {' . PHP_EOL;
+ if ( isset( $this->options['autoplay'] ) && $this->options['autoplay'] === true ) {
+ $html .= "jQuery.VideoPress.video.play(jQuery({$jq_container}));";
+ } else {
+ $html .= 'jQuery(' . $jq_placeholder . ').one("click",function(){jQuery.VideoPress.video.play(jQuery(' . $jq_container . '))});';
+ }
+ $html .= '}';
+
+ // close the jQuery(document).ready() function
+ $html .= '});';
+ }
+ $html .= '</script>' . PHP_EOL;
+ $html .= '</div>' . PHP_EOL;
+
+ /*
+ * JavaScript required
+ */
+ $noun = __( 'this video', 'jetpack' );
+ if ( ! $age_gate_required ) {
+ $vid_type = '';
+ if ( ( isset( $this->options['freedom'] ) && $this->options['freedom'] === true ) && ( isset( $this->video->videos->ogv ) && isset( $this->video->videos->ogv->url ) ) ) {
+ $vid_type = 'ogv';
+ } elseif ( isset( $this->video->videos->mp4 ) && isset( $this->video->videos->mp4->url ) ) {
+ $vid_type = 'mp4';
+ } elseif ( isset( $this->video->videos->ogv ) && isset( $this->video->videos->ogv->url ) ) {
+ $vid_type = 'ogv';
+ }
+
+ if ( $vid_type !== '' ) {
+ $noun = '<a ';
+ if ( isset( $this->video->language ) ) {
+ $noun .= 'hreflang="' . esc_attr( $this->video->language ) . '" ';
+ }
+ if ( $vid_type === 'mp4' ) {
+ $noun .= 'type="video/mp4" href="' . esc_url( $this->video->videos->mp4->url, array( 'http', 'https' ) );
+ } elseif ( $vid_type === 'ogv' ) {
+ $noun .= 'type="video/ogv" href="' . esc_url( $this->video->videos->ogv->url, array( 'http', 'https' ) );
+ }
+ $noun .= '">';
+ if ( isset( $this->video->title ) ) {
+ $noun .= esc_html( $this->video->title );
+ } else {
+ $noun .= __( 'this video', 'jetpack' );
+ }
+ $noun .= '</a>';
+ } elseif ( ! empty( $this->title ) ) {
+ $noun = esc_html( $this->title );
+ }
+ unset( $vid_type );
+ }
+ $html .= '<noscript><p>' . sprintf( _x( 'JavaScript required to play %s.', 'Play as in playback or view a movie', 'jetpack' ), $noun ) . '</p></noscript>';
+
+ return $html;
+ }
+
+ function html5_dynamic_next() {
+ $video_container_id = 'v-' . $this->video->guid;
+
+ // Must not use iframes for IE11 due to a fullscreen bug
+ if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && stristr( $_SERVER['HTTP_USER_AGENT'], 'Trident/7.0; rv:11.0' ) ) {
+ $iframe_embed = false;
+ } else {
+
+ /**
+ * Filter the VideoPress iframe embed
+ *
+ * This filter allows you to control whether the videos will be embedded using an iframe.
+ * Set this to false in order to use an in-page embed rather than an iframe.
+ *
+ * @module videopress
+ *
+ * @since 3.7.0
+ *
+ * @param boolean $videopress_player_use_iframe
+ */
+ $iframe_embed = apply_filters( 'jetpack_videopress_player_use_iframe', true );
+ }
+
+ if ( ! array_key_exists( 'hd', $this->options ) ) {
+ $this->options['hd'] = (bool) get_option( 'video_player_high_quality', false );
+ }
+
+ $videopress_options = array(
+ 'width' => absint( $this->video->calculated_width ),
+ 'height' => absint( $this->video->calculated_height ),
+ );
+ foreach ( $this->options as $option => $value ) {
+ switch ( $option ) {
+ case 'at':
+ if ( intval( $value ) ) {
+ $videopress_options[ $option ] = intval( $value );
+ }
+ break;
+ case 'autoplay':
+ $option = 'autoPlay';
+ case 'hd':
+ case 'loop':
+ case 'permalink':
+ if ( in_array( $value, array( 1, 'true' ) ) ) {
+ $videopress_options[ $option ] = true;
+ } elseif ( in_array( $value, array( 0, 'false' ) ) ) {
+ $videopress_options[ $option ] = false;
+ }
+ break;
+ case 'defaultlangcode':
+ $option = 'defaultLangCode';
+ if ( $value ) {
+ $videopress_options[ $option ] = $value;
+ }
+ break;
+ }
+ }
+
+ if ( $iframe_embed ) {
+ $iframe_url = "https://videopress.com/embed/{$this->video->guid}";
+
+ foreach ( $videopress_options as $option => $value ) {
+ if ( ! in_array( $option, array( 'width', 'height' ) ) ) {
+
+ // add_query_arg ignores false as a value, so replacing it with 0
+ $iframe_url = add_query_arg( $option, ( false === $value ) ? 0 : $value, $iframe_url );
+ }
+ }
+
+ $js_url = 'https://s0.wp.com/wp-content/plugins/video/assets/js/next/videopress-iframe.js';
+
+ return "<iframe width='" . esc_attr( $videopress_options['width'] )
+ . "' height='" . esc_attr( $videopress_options['height'] )
+ . "' src='" . esc_attr( $iframe_url )
+ . "' frameborder='0' allowfullscreen></iframe>"
+ . "<script src='" . esc_attr( $js_url ) . "'></script>";
+
+ } else {
+ $videopress_options = json_encode( $videopress_options );
+ $js_url = 'https://s0.wp.com/wp-content/plugins/video/assets/js/next/videopress.js';
+
+ return "<div id='{$video_container_id}'></div>
+ <script src='{$js_url}'></script>
+ <script>
+ videopress('{$this->video->guid}', document.querySelector('#{$video_container_id}'), {$videopress_options});
+ </script>";
+ }
+ }
+
+ /**
+ * Only allow legitimate Flash parameters and their values
+ *
+ * @since 1.2
+ * @link http://kb2.adobe.com/cps/127/tn_12701.html Flash object and embed attributes
+ * @link http://kb2.adobe.com/cps/133/tn_13331.html devicefont
+ * @link http://kb2.adobe.com/cps/164/tn_16494.html allowscriptaccess
+ * @link http://www.adobe.com/devnet/flashplayer/articles/full_screen_mode.html full screen mode
+ * @link http://livedocs.adobe.com/flash/9.0/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00001079.html allownetworking
+ * @param array $flash_params Flash parameters expressed in key-value form
+ * @return array validated Flash parameters
+ */
+ public static function esc_flash_params( $flash_params ) {
+ $allowed_params = array(
+ 'swliveconnect' => array( 'true', 'false' ),
+ 'play' => array( 'true', 'false' ),
+ 'loop' => array( 'true', 'false' ),
+ 'menu' => array( 'true', 'false' ),
+ 'quality' => array( 'low', 'autolow', 'autohigh', 'medium', 'high', 'best' ),
+ 'scale' => array( 'default', 'noborder', 'exactfit', 'noscale' ),
+ 'align' => array( 'l', 'r', 't' ),
+ 'salign' => array( 'l', 'r', 't', 'tl', 'tr', 'bl', 'br' ),
+ 'wmode' => array( 'window', 'opaque', 'transparent', 'direct', 'gpu' ),
+ 'devicefont' => array( '_sans', '_serif', '_typewriter' ),
+ 'allowscriptaccess' => array( 'always', 'samedomain', 'never' ),
+ 'allownetworking' => array( 'all', 'internal', 'none' ),
+ 'seamlesstabbing' => array( 'true', 'false' ),
+ 'allowfullscreen' => array( 'true', 'false' ),
+ 'fullScreenAspectRatio' => array( 'portrait', 'landscape' ),
+ 'base',
+ 'bgcolor',
+ 'flashvars',
+ );
+
+ $allowed_params_keys = array_keys( $allowed_params );
+
+ $filtered_params = array();
+ foreach ( $flash_params as $param => $value ) {
+ if ( empty( $param ) || empty( $value ) ) {
+ continue;
+ }
+ $param = strtolower( $param );
+ if ( in_array( $param, $allowed_params_keys ) ) {
+ if ( isset( $allowed_params[ $param ] ) && is_array( $allowed_params[ $param ] ) ) {
+ $value = strtolower( $value );
+ if ( in_array( $value, $allowed_params[ $param ] ) ) {
+ $filtered_params[ $param ] = $value;
+ }
+ } else {
+ $filtered_params[ $param ] = $value;
+ }
+ }
+ }
+ unset( $allowed_params_keys );
+
+ /**
+ * Flash specifies sameDomain, not samedomain. change from lowercase value for preciseness
+ */
+ if ( isset( $filtered_params['allowscriptaccess'] ) && $filtered_params['allowscriptaccess'] === 'samedomain' ) {
+ $filtered_params['allowscriptaccess'] = 'sameDomain';
+ }
+
+ return $filtered_params;
+ }
+
+ /**
+ * Filter Flash variables from the response, taking into consideration player options.
+ *
+ * @since 1.3
+ * @return array Flash variable key value pairs
+ */
+ private function get_flash_variables() {
+ if ( ! isset( $this->video->players->swf->vars ) ) {
+ return array();
+ }
+
+ $flashvars = (array) $this->video->players->swf->vars;
+ if ( isset( $this->options['autoplay'] ) && $this->options['autoplay'] === true ) {
+ $flashvars['autoPlay'] = 'true';
+ }
+ return $flashvars;
+ }
+
+ /**
+ * Validate and filter Flash parameters
+ *
+ * @since 1.3
+ * @return array Flash parameters passed through key and value validation
+ */
+ private function get_flash_parameters() {
+ if ( ! isset( $this->video->players->swf->params ) ) {
+ return array();
+ } else {
+ return self::esc_flash_params(
+ /**
+ * Filters the Flash parameters of the VideoPress player.
+ *
+ * @module videopress
+ *
+ * @since 1.2.0
+ *
+ * @param array $this->video->players->swf->params Array of swf parameters for the VideoPress flash player.
+ */
+ apply_filters( 'video_flash_params', (array) $this->video->players->swf->params, 10, 1 )
+ );
+ }
+ }
+
+ /**
+ * Flash player markup in a HTML embed element.
+ *
+ * @since 1.1
+ * @link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-iframe-element.html#the-embed-element embed element
+ * @link http://www.google.com/support/reader/bin/answer.py?answer=70664 Google Reader markup support
+ * @return string HTML markup. Embed element with no children
+ */
+ private function flash_embed() {
+ wp_enqueue_script( 'videopress' );
+ if ( ! isset( $this->video->players->swf ) || ! isset( $this->video->players->swf->url ) ) {
+ return '';
+ }
+
+ $embed = array(
+ 'id' => $this->video_id,
+ 'src' => esc_url_raw( $this->video->players->swf->url . '&' . http_build_query( $this->get_flash_variables(), null, '&' ), array( 'http', 'https' ) ),
+ 'type' => 'application/x-shockwave-flash',
+ 'width' => $this->video->calculated_width,
+ 'height' => $this->video->calculated_height,
+ );
+ if ( isset( $this->video->title ) ) {
+ $embed['title'] = $this->video->title;
+ }
+ $embed = array_merge( $embed, $this->get_flash_parameters() );
+
+ $html = '<embed';
+ foreach ( $embed as $attribute => $value ) {
+ $html .= ' ' . esc_html( $attribute ) . '="' . esc_attr( $value ) . '"';
+ }
+ unset( $embed );
+ $html .= '></embed>';
+ return $html;
+ }
+
+ /**
+ * Double-baked Flash object markup for Internet Explorer and more standards-friendly consuming agents.
+ *
+ * @since 1.1
+ * @return HTML markup. Object and children.
+ */
+ private function flash_object() {
+ wp_enqueue_script( 'videopress' );
+ if ( ! isset( $this->video->players->swf ) || ! isset( $this->video->players->swf->url ) ) {
+ return '';
+ }
+
+ $thumbnail_html = '<img alt="';
+ if ( isset( $this->video->title ) ) {
+ $thumbnail_html .= esc_attr( $this->video->title );
+ }
+ $thumbnail_html .= '" src="' . esc_url( $this->video->poster_frame_uri, array( 'http', 'https' ) ) . '" width="' . $this->video->calculated_width . '" height="' . $this->video->calculated_height . '" />';
+ $flash_vars = esc_attr( http_build_query( $this->get_flash_variables(), null, '&' ) );
+ $flash_params = '';
+ foreach ( $this->get_flash_parameters() as $attribute => $value ) {
+ $flash_params .= '<param name="' . esc_attr( $attribute ) . '" value="' . esc_attr( $value ) . '" />';
+ }
+ $flash_help = sprintf( __( 'This video requires <a rel="nofollow noopener noreferrer" href="%s" target="_blank">Adobe Flash</a> for playback.', 'jetpack' ), 'http://www.adobe.com/go/getflashplayer' );
+ $flash_player_url = esc_url( $this->video->players->swf->url, array( 'http', 'https' ) );
+ $description = '';
+ if ( isset( $this->video->title ) ) {
+ $standby = $this->video->title;
+ $description = '<p><strong>' . esc_html( $this->video->title ) . '</strong></p>';
+ } else {
+ $standby = __( 'Loading video...', 'jetpack' );
+ }
+ $standby = ' standby="' . esc_attr( $standby ) . '"';
+ return <<<OBJECT
+<script type="text/javascript">if(typeof swfobject!=="undefined"){swfobject.registerObject("{$this->video_id}", "{$this->video->players->swf->version}");}</script>
+<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="{$this->video->calculated_width}" height="{$this->video->calculated_height}" id="{$this->video_id}"{$standby}>
+ <param name="movie" value="{$flash_player_url}" />
+ {$flash_params}
+ <param name="flashvars" value="{$flash_vars}" />
+ <!--[if !IE]>-->
+ <object type="application/x-shockwave-flash" data="{$flash_player_url}" width="{$this->video->calculated_width}" height="{$this->video->calculated_height}"{$standby}>
+ {$flash_params}
+ <param name="flashvars" value="{$flash_vars}" />
+ <!--<![endif]-->
+ {$thumbnail_html}{$description}<p class="robots-nocontent">{$flash_help}</p>
+ <!--[if !IE]>-->
+ </object>
+ <!--<![endif]-->
+</object>
+OBJECT;
+ }
+}
diff --git a/plugins/jetpack/modules/videopress/class.videopress-scheduler.php b/plugins/jetpack/modules/videopress/class.videopress-scheduler.php
new file mode 100644
index 00000000..bff33684
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/class.videopress-scheduler.php
@@ -0,0 +1,197 @@
+<?php
+/**
+ * VideoPress playback module markup generator.
+ *
+ * @since 1.3
+ */
+class VideoPress_Scheduler {
+
+ /**
+ * The name of the function used to run the cleanup cron.
+ */
+ const CLEANUP_CRON_METHOD = 'videopress_cleanup_media_library';
+
+ /**
+ * @var VideoPress_Scheduler
+ **/
+ private static $instance = null;
+
+ /**
+ * A list of all of the crons that are to be activated, along with their interval timings.
+ *
+ * @var array
+ */
+ protected $crons = array(
+ // 'cleanup' => array(
+ // 'method' => self::CLEANUP_CRON_METHOD,
+ // 'interval' => 'minutes_30',
+ // ),
+ );
+
+
+ /**
+ * Private VideoPress_Scheduler constructor.
+ *
+ * Use the VideoPress_Scheduler::init() method to get an instance.
+ */
+ private function __construct() {
+ add_filter( 'cron_schedules', array( $this, 'add_30_minute_cron_interval' ) );
+
+ // Activate the cleanup cron if videopress is enabled, jetpack is activated, or jetpack is updated.
+ add_action( 'jetpack_activate_module_videopress', array( $this, 'activate_all_crons' ) );
+ add_action( 'updating_jetpack_version', array( $this, 'activate_all_crons' ) );
+ add_action( 'activated_plugin', array( $this, 'activate_crons_on_jetpack_activation' ) );
+
+ // Deactivate the cron if either videopress is disabled or Jetpack is disabled.
+ add_action( 'jetpack_deactivate_module_videopress', array( $this, 'deactivate_all_crons' ) );
+ register_deactivation_hook( plugin_basename( JETPACK__PLUGIN_FILE ), array( $this, 'deactivate_all_crons' ) );
+ }
+
+ /**
+ * Initialize the VideoPress_Scheduler and get back a singleton instance.
+ *
+ * @return VideoPress_Scheduler
+ */
+ public static function init() {
+ if ( is_null( self::$instance ) ) {
+ self::$instance = new VideoPress_Scheduler();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Adds 30 minute running interval to the cron schedules.
+ *
+ * @param array $current_schedules Currently defined schedules list.
+ *
+ * @return array
+ */
+ public function add_30_minute_cron_interval( $current_schedules ) {
+
+ // Only add the 30 minute interval if it wasn't already set.
+ if ( ! isset( $current_schedules['minutes_30'] ) ) {
+ $current_schedules['minutes_30'] = array(
+ 'interval' => 30 * MINUTE_IN_SECONDS,
+ 'display' => 'Every 30 minutes',
+ );
+ }
+
+ return $current_schedules;
+ }
+
+ /**
+ * Activate a single cron
+ *
+ * @param string $cron_name
+ *
+ * @return bool
+ */
+ public function activate_cron( $cron_name ) {
+
+ if ( ! $this->is_cron_valid( $cron_name ) ) {
+ return false;
+ }
+
+ if ( ! $this->check_cron( $cron_name ) ) {
+ wp_schedule_event( time(), $this->crons[ $cron_name ]['interval'], $this->crons[ $cron_name ]['method'] );
+ }
+ }
+
+ /**
+ * Activates widget update cron task.
+ */
+ public function activate_all_crons() {
+
+ if ( ! Jetpack::is_module_active( 'videopress' ) ) {
+ return false;
+ }
+
+ foreach ( $this->crons as $cron_name => $cron ) {
+ if ( ! $this->check_cron( $cron_name ) ) {
+ wp_schedule_event( time(), $cron['interval'], $cron['method'] );
+ }
+ }
+ }
+
+ /**
+ * Only activate the crons if it is Jetpack that was activated.
+ *
+ * @param string $plugin_file_name
+ */
+ public function activate_crons_on_jetpack_activation( $plugin_file_name ) {
+
+ if ( plugin_basename( JETPACK__PLUGIN_FILE ) === $plugin_file_name ) {
+ $this->activate_all_crons();
+ }
+ }
+
+ /**
+ * Deactivates any crons associated with the VideoPress module.
+ *
+ * @return bool
+ */
+ public function deactivate_cron( $cron_name ) {
+
+ if ( ! $this->is_cron_valid( $cron_name ) ) {
+ return false;
+ }
+
+ $next_scheduled_time = $this->check_cron( $cron_name );
+ wp_unschedule_event( $next_scheduled_time, $this->crons[ $cron_name ]['method'] );
+
+ return true;
+ }
+
+ /**
+ * Deactivates any crons associated with the VideoPress module..
+ */
+ public function deactivate_all_crons() {
+
+ foreach ( $this->crons as $cron_name => $cron ) {
+ $this->deactivate_cron( $cron_name );
+ }
+ }
+
+ /**
+ * Is the given cron job currently active?
+ *
+ * If so, return when it will next run,
+ *
+ * @param string $cron_name
+ *
+ * @return int|bool Timestamp of the next run time OR false.
+ */
+ public function check_cron( $cron_name ) {
+ if ( ! $this->is_cron_valid( $cron_name ) ) {
+ return false;
+ }
+
+ return wp_next_scheduled( $this->crons[ $cron_name ]['method'] );
+ }
+
+ /**
+ * Check that the given cron job name is valid.
+ *
+ * @param string $cron_name
+ *
+ * @return bool
+ */
+ public function is_cron_valid( $cron_name ) {
+
+ if ( ! isset( $this->crons[ $cron_name ]['method'] ) || ! isset( $this->crons[ $cron_name ]['interval'] ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get a list of all of the crons that are available.
+ *
+ * @return array
+ */
+ public function get_crons() {
+ return $this->crons;
+ }
+}
diff --git a/plugins/jetpack/modules/videopress/class.videopress-video.php b/plugins/jetpack/modules/videopress/class.videopress-video.php
new file mode 100644
index 00000000..a8c3a6b8
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/class.videopress-video.php
@@ -0,0 +1,378 @@
+<?php
+/**
+ * VideoPress video object retrieved from VideoPress servers and parsed.
+ *
+ * @since 1.3
+ */
+class VideoPress_Video {
+ public $version = 3;
+
+ /**
+ * Manifest version returned by remote service.
+ *
+ * @var string
+ * @since 1.3
+ */
+ const manifest_version = '1.5';
+
+ /**
+ * Expiration of the video expressed in Unix time
+ *
+ * @var int
+ * @since 1.3
+ */
+ public $expires;
+
+ /**
+ * VideoPress unique identifier
+ *
+ * @var string
+ * @since 1.3
+ */
+ public $guid;
+
+ /**
+ * WordPress.com blog identifier
+ *
+ * @var int
+ * @since 1.5
+ */
+ public $blog_id;
+
+ /**
+ * Remote blog attachment identifier
+ *
+ * @var int
+ * @since 1.5
+ */
+ public $post_id;
+
+ /**
+ * Maximum desired width.
+ *
+ * @var int
+ * @since 1.3
+ */
+ public $maxwidth;
+
+ /**
+ * Video width calculated based on original video dimensions and the requested maxwidth
+ *
+ * @var int
+ * @since 1.3
+ */
+ public $calculated_width;
+
+ /**
+ * Video height calculated based on original video dimensions and the requested maxwidth
+ *
+ * @var int
+ * @since 1.3
+ */
+ public $calculated_height;
+
+ /**
+ * Video title
+ *
+ * @var string
+ * @since 1.3
+ */
+ public $title;
+
+ /**
+ * Video description
+ *
+ * @var string
+ * @since 4.4
+ */
+ public $description;
+
+ /**
+ * Directionality of title text. ltr or rtl
+ *
+ * @var string
+ * @since 1.3
+ */
+ public $text_direction;
+
+ /**
+ * Text and audio language as ISO 639-2 language code
+ *
+ * @var string
+ * @since 1.3
+ */
+ public $language;
+
+ /**
+ * Video duration in whole seconds
+ *
+ * @var int
+ * @since 1.3
+ */
+ public $duration;
+
+ /**
+ * Recommended minimum age of the viewer.
+ *
+ * @var int
+ * @since 1.3
+ */
+ public $age_rating;
+
+ /**
+ * Video author has restricted video embedding or sharing
+ *
+ * @var bool
+ * @since 1.3
+ */
+ public $restricted_embed;
+
+ /**
+ * Poster frame image URI for the given video guid and calculated dimensions.
+ *
+ * @var string
+ * @since 1.3
+ */
+ public $poster_frame_uri;
+
+ /**
+ * Video files associated with the given guid for the calculated dimensions.
+ *
+ * @var stdClass
+ * @since 1.3
+ */
+ public $videos;
+
+ /**
+ * Video player information
+ *
+ * @var stdClass
+ * @since 1.3
+ */
+ public $players;
+
+ /**
+ * Video player skinning preferences including background color and watermark
+ *
+ * @var array
+ * @since 1.5
+ */
+ public $skin;
+
+ /**
+ * Closed captions if available for the given video. Associative array of ISO 639-2 language code and a WebVTT URI
+ *
+ * @var array
+ * @since 1.5
+ */
+ public $captions;
+
+ /**
+ * Setup the object.
+ * Request video information from VideoPress servers and process the response.
+ *
+ * @since 1.3
+ * @var string $guid VideoPress unique identifier
+ * @var int $maxwidth maximum requested video width. final width and height are calculated on VideoPress servers based on the aspect ratio of the original video upload.
+ */
+ public function __construct( $guid, $maxwidth = 640 ) {
+ $this->guid = $guid;
+
+ $maxwidth = absint( $maxwidth );
+ if ( $maxwidth > 0 ) {
+ $this->maxwidth = $maxwidth;
+ }
+
+ $data = $this->get_data();
+ if ( is_wp_error( $data ) || empty( $data ) ) {
+ /** This filter is documented in modules/videopress/class.videopress-player.php */
+ if ( ! apply_filters( 'jetpack_videopress_use_legacy_player', false ) ) {
+ // Unlike the Flash player, the new player does it's own error checking, age gate, etc.
+ $data = (object) array(
+ 'guid' => $guid,
+ 'width' => $maxwidth,
+ 'height' => $maxwidth / 16 * 9,
+ );
+ } else {
+ $this->error = $data;
+ return;
+ }
+ }
+
+ if ( isset( $data->blog_id ) ) {
+ $this->blog_id = absint( $data->blog_id );
+ }
+
+ if ( isset( $data->post_id ) ) {
+ $this->post_id = absint( $data->post_id );
+ }
+
+ if ( isset( $data->title ) && $data->title !== '' ) {
+ $this->title = trim( str_replace( '&nbsp;', ' ', $data->title ) );
+ }
+
+ if ( isset( $data->description ) && $data->description !== '' ) {
+ $this->description = trim( $data->description );
+ }
+
+ if ( isset( $data->text_direction ) && $data->text_direction === 'rtl' ) {
+ $this->text_direction = 'rtl';
+ } else {
+ $this->text_direction = 'ltr';
+ }
+
+ if ( isset( $data->language ) ) {
+ $this->language = $data->language;
+ }
+
+ if ( isset( $data->duration ) && $data->duration > 0 ) {
+ $this->duration = absint( $data->duration );
+ }
+
+ if ( isset( $data->width ) && $data->width > 0 ) {
+ $this->calculated_width = absint( $data->width );
+ }
+
+ if ( isset( $data->height ) && $data->height > 0 ) {
+ $this->calculated_height = absint( $data->height );
+ }
+
+ if ( isset( $data->age_rating ) ) {
+ $this->age_rating = absint( $this->age_rating );
+ }
+
+ if ( isset( $data->restricted_embed ) && $data->restricted_embed === true ) {
+ $this->restricted_embed = true;
+ } else {
+ $this->restricted_embed = false;
+ }
+
+ if ( isset( $data->posterframe ) && $data->posterframe !== '' ) {
+ $this->poster_frame_uri = esc_url_raw( $data->posterframe, array( 'http', 'https' ) );
+ }
+
+ if ( isset( $data->mp4 ) || isset( $data->ogv ) ) {
+ $this->videos = new stdClass();
+ if ( isset( $data->mp4 ) ) {
+ $this->videos->mp4 = $data->mp4;
+ }
+ if ( isset( $data->ogv ) ) {
+ $this->videos->ogv = $data->ogv;
+ }
+ }
+
+ if ( isset( $data->swf ) ) {
+ if ( ! isset( $this->players ) ) {
+ $this->players = new stdClass();
+ }
+ $this->players->swf = $data->swf;
+ }
+
+ if ( isset( $data->skin ) ) {
+ $this->skin = $data->skin;
+ }
+
+ if ( isset( $data->captions ) ) {
+ $this->captions = (array) $data->captions;
+ }
+ }
+
+ /**
+ * Convert an Expires HTTP header value into Unix time for use in WP Cache
+ *
+ * @since 1.3
+ * @var string $expires_header
+ * @return int|bool Unix time or false
+ */
+ public static function calculate_expiration( $expires_header ) {
+ if ( empty( $expires_header ) || ! is_string( $expires_header ) ) {
+ return false;
+ }
+
+ if (
+ class_exists( 'DateTimeZone' )
+ && method_exists( 'DateTime', 'createFromFormat' )
+ ) {
+ $expires_date = DateTime::createFromFormat( 'D, d M Y H:i:s T', $expires_header, new DateTimeZone( 'UTC' ) );
+ if ( $expires_date instanceof DateTime ) {
+ return date_format( $expires_date, 'U' );
+ }
+ } else {
+ $expires_array = strptime( $expires_header, '%a, %d %b %Y %H:%M:%S %Z' );
+ if ( is_array( $expires_array ) && isset( $expires_array['tm_hour'] ) && isset( $expires_array['tm_min'] ) && isset( $expires_array['tm_sec'] ) && isset( $expires_array['tm_mon'] ) && isset( $expires_array['tm_mday'] ) && isset( $expires_array['tm_year'] ) ) {
+ return gmmktime( $expires_array['tm_hour'], $expires_array['tm_min'], $expires_array['tm_sec'], 1 + $expires_array['tm_mon'], $expires_array['tm_mday'], 1900 + $expires_array['tm_year'] );
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Extract the site's host domain for statistics and comparison against an allowed site list in the case of restricted embeds.
+ *
+ * @since 1.2
+ * @param string $url absolute URL
+ * @return bool|string host component of the URL, or false if none found
+ */
+ public static function hostname( $url ) {
+ return parse_url( esc_url_raw( $url ), PHP_URL_HOST );
+ }
+
+
+ /**
+ * Request data from WordPress.com for the given guid, maxwidth, and calculated blog hostname.
+ *
+ * @since 1.3
+ * @return stdClass|WP_Error parsed JSON response or WP_Error if request unsuccessful
+ */
+ private function get_data() {
+ global $wp_version;
+
+ $domain = self::hostname( home_url() );
+ $request_params = array(
+ 'guid' => $this->guid,
+ 'domain' => $domain,
+ );
+ if ( isset( $this->maxwidth ) && $this->maxwidth > 0 ) {
+ $request_params['maxwidth'] = $this->maxwidth;
+ }
+
+ $url = 'http://videopress.com/data/wordpress.json';
+ if ( is_ssl() ) {
+ $url = 'https://v.wordpress.com/data/wordpress.json';
+ }
+
+ $response = wp_remote_get(
+ add_query_arg( $request_params, $url ),
+ array(
+ 'redirection' => 1,
+ 'user-agent' => 'VideoPress plugin ' . $this->version . '; WordPress ' . $wp_version . ' (' . home_url( '/' ) . ')',
+ )
+ );
+
+ unset( $request_params );
+ unset( $url );
+ $response_body = wp_remote_retrieve_body( $response );
+ $response_code = absint( wp_remote_retrieve_response_code( $response ) );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ } elseif ( $response_code === 400 ) {
+ return new WP_Error( 'bad_config', __( 'The VideoPress plugin could not communicate with the VideoPress servers. This error is most likely caused by a misconfigured plugin. Please reinstall or upgrade.', 'jetpack' ) );
+ } elseif ( $response_code === 403 ) {
+ return new WP_Error( 'http_forbidden', '<p>' . sprintf( __( '<strong>%s</strong> is not an allowed embed site.', 'jetpack' ), esc_html( $domain ) ) . '</p><p>' . __( 'Publisher limits playback of video embeds.', 'jetpack' ) . '</p>' );
+ } elseif ( $response_code === 404 ) {
+ return new WP_Error( 'http_not_found', '<p>' . sprintf( __( 'No data found for VideoPress identifier: <strong>%s</strong>.', 'jetpack' ), $this->guid ) . '</p>' );
+ } elseif ( $response_code !== 200 || empty( $response_body ) ) {
+ return;
+ } else {
+ $expires_header = wp_remote_retrieve_header( $response, 'Expires' );
+ if ( ! empty( $expires_header ) ) {
+ $expires = self::calculate_expiration( $expires_header );
+ if ( ! empty( $expires ) ) {
+ $this->expires = $expires;
+ }
+ }
+ return json_decode( $response_body );
+ }
+ }
+}
diff --git a/plugins/jetpack/modules/videopress/class.videopress-xmlrpc.php b/plugins/jetpack/modules/videopress/class.videopress-xmlrpc.php
new file mode 100644
index 00000000..73c67d7d
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/class.videopress-xmlrpc.php
@@ -0,0 +1,173 @@
+<?php
+/**
+ * VideoPress playback module markup generator.
+ *
+ * @since 1.3
+ */
+class VideoPress_XMLRPC {
+
+ /**
+ * @var VideoPress_XMLRPC
+ **/
+ private static $instance = null;
+
+
+ /**
+ * Private VideoPress_XMLRPC constructor.
+ *
+ * Use the VideoPress_XMLRPC::init() method to get an instance.
+ */
+ private function __construct() {
+ add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
+ }
+
+ /**
+ * Initialize the VideoPress_XMLRPC and get back a singleton instance.
+ *
+ * @return VideoPress_XMLRPC
+ */
+ public static function init() {
+ if ( is_null( self::$instance ) ) {
+ self::$instance = new VideoPress_XMLRPC();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Adds additional methods the WordPress xmlrpc API for handling VideoPress specific features
+ *
+ * @param array $methods
+ *
+ * @return array
+ */
+ public function xmlrpc_methods( $methods ) {
+
+ $methods['jetpack.createMediaItem'] = array( $this, 'create_media_item' );
+ $methods['jetpack.updateVideoPressMediaItem'] = array( $this, 'update_videopress_media_item' );
+ $methods['jetpack.updateVideoPressPosterImage'] = array( $this, 'update_poster_image' );
+
+ return $methods;
+ }
+
+ /**
+ * This is used by the WPCOM VideoPress uploader in order to create a media item with
+ * specific meta data about an uploaded file. After this, the transcoding session will
+ * update the meta information via the update_videopress_media_item() method.
+ *
+ * Note: This method technically handles the creation of multiple media objects, though
+ * in practice this is never done.
+ *
+ * @param array $media
+ * @return array
+ */
+ public function create_media_item( $media ) {
+ foreach ( $media as & $media_item ) {
+ $title = sanitize_title( basename( $media_item['url'] ) );
+ $guid = isset( $media['guid'] ) ? $media['guid'] : null;
+
+ $media_id = videopress_create_new_media_item( $title, $guid );
+
+ wp_update_attachment_metadata(
+ $media_id,
+ array(
+ 'original' => array(
+ 'url' => $media_item['url'],
+ ),
+ )
+ );
+
+ $media_item['post'] = get_post( $media_id );
+ }
+
+ return array( 'media' => $media );
+ }
+
+ /**
+ * @param array $request
+ *
+ * @return bool
+ */
+ public function update_videopress_media_item( $request ) {
+
+ $id = $request['post_id'];
+ $status = $request['status'];
+ $format = $request['format'];
+ $info = $request['info'];
+
+ if ( ! $attachment = get_post( $id ) ) {
+ return false;
+ }
+
+ $attachment->guid = $info['original'];
+
+ wp_update_post( $attachment );
+
+ // Update the vp guid and set it to a direct meta property.
+ update_post_meta( $id, 'videopress_guid', $info['guid'] );
+
+ $meta = wp_get_attachment_metadata( $id );
+
+ $meta['width'] = $info['width'];
+ $meta['height'] = $info['height'];
+ $meta['original']['url'] = $info['original'];
+ $meta['videopress'] = $info;
+ $meta['videopress']['url'] = 'https://videopress.com/v/' . $info['guid'];
+
+ // Update file statuses
+ $valid_formats = array( 'hd', 'ogg', 'mp4', 'dvd' );
+ if ( in_array( $format, $valid_formats ) ) {
+ $meta['file_statuses'][ $format ] = $status;
+ }
+
+ if ( ! get_post_meta( $id, '_thumbnail_id', true ) ) {
+ // Update the poster in the VideoPress info.
+ $thumbnail_id = videopress_download_poster_image( $info['poster'], $id );
+
+ if ( is_int( $thumbnail_id ) ) {
+ update_post_meta( $id, '_thumbnail_id', $thumbnail_id );
+ }
+ }
+
+ wp_update_attachment_metadata( $id, $meta );
+
+ videopress_update_meta_data( $id );
+
+ // update the meta to tell us that we're processing or complete
+ update_post_meta( $id, 'videopress_status', videopress_is_finished_processing( $id ) ? 'complete' : 'processing' );
+
+ return true;
+ }
+
+ /**
+ * @param array $request
+ * @return bool
+ */
+ public function update_poster_image( $request ) {
+
+ $post_id = $request['post_id'];
+ $poster = $request['poster'];
+
+ if ( ! $attachment = get_post( $post_id ) ) {
+ return false;
+ }
+
+ // We add ssl => 1 to make sure that the videos.files.wordpress.com domain is parsed as photon.
+ $poster = apply_filters( 'jetpack_photon_url', $poster, array( 'ssl' => 1 ), 'https' );
+
+ $meta = wp_get_attachment_metadata( $post_id );
+ $meta['videopress']['poster'] = $poster;
+ wp_update_attachment_metadata( $post_id, $meta );
+
+ // Update the poster in the VideoPress info.
+ $thumbnail_id = videopress_download_poster_image( $poster, $post_id );
+
+ if ( ! is_int( $thumbnail_id ) ) {
+ return false;
+ }
+
+ update_post_meta( $post_id, '_thumbnail_id', $thumbnail_id );
+
+ return true;
+ }
+}
diff --git a/plugins/jetpack/modules/videopress/css/editor-rtl.css b/plugins/jetpack/modules/videopress/css/editor-rtl.css
new file mode 100644
index 00000000..12945cb6
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/css/editor-rtl.css
@@ -0,0 +1,60 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+/* VideoPress Settings Modal style overrides */
+.mce-videopress-field-guid,
+.mce-videopress-field-freedom,
+.mce-videopress-field-flashonly {
+ display: none;
+}
+
+.mce-videopress-checkbox .mce-checkbox {
+ right: 120px !important;
+ width: 100% !important; /* assigning a full width so the label area is clickable */
+}
+
+.mce-videopress-checkbox .mce-label {
+ right: 150px !important;
+}
+
+.mce-videopress-checkbox .mce-label-unit {
+ position: absolute;
+ right: 210px;
+ top: 5px;
+}
+
+.mce-videopress-checkbox i.mce-i-checkbox {
+ background-color: #fff;
+ color: #1e8cbe;
+}
+
+.mce-videopress-checkbox .mce-i-checkbox:before {
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ font: 400 21px/1 dashicons;
+ speak: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ margin: -3px -3px 0 0;
+ content: "\f147";
+}
+
+.mce-videopress-checkbox .mce-i-checkbox.mce-checked:before {
+ content: "\f147";
+}
+
+div[class*=mce-videopress-field] input[type=number] {
+ width: 70px !important;
+ right: 120px !important;
+}
+
+.mce-videopress-field-w .mce-label,
+.mce-videopress-field-at .mce-label {
+ width: 115px !important;
+ text-align: left;
+}
+
+.mce-videopress-field-unit {
+ position: absolute;
+ right: 210px;
+ top: 5px;
+}
diff --git a/plugins/jetpack/modules/videopress/css/editor-rtl.min.css b/plugins/jetpack/modules/videopress/css/editor-rtl.min.css
new file mode 100644
index 00000000..460d6f03
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/css/editor-rtl.min.css
@@ -0,0 +1 @@
+.mce-videopress-field-flashonly,.mce-videopress-field-freedom,.mce-videopress-field-guid{display:none}.mce-videopress-checkbox .mce-checkbox{right:120px!important;width:100%!important}.mce-videopress-checkbox .mce-label{right:150px!important}.mce-videopress-checkbox .mce-label-unit{position:absolute;right:210px;top:5px}.mce-videopress-checkbox i.mce-i-checkbox{background-color:#fff;color:#1e8cbe}.mce-videopress-checkbox .mce-i-checkbox:before{display:inline-block;vertical-align:middle;width:16px;font:400 21px/1 dashicons;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin:-3px -3px 0 0;content:"\f147"}.mce-videopress-checkbox .mce-i-checkbox.mce-checked:before{content:"\f147"}div[class*=mce-videopress-field] input[type=number]{width:70px!important;right:120px!important}.mce-videopress-field-at .mce-label,.mce-videopress-field-w .mce-label{width:115px!important;text-align:left}.mce-videopress-field-unit{position:absolute;right:210px;top:5px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/videopress/css/editor.css b/plugins/jetpack/modules/videopress/css/editor.css
new file mode 100644
index 00000000..69b79dfe
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/css/editor.css
@@ -0,0 +1,59 @@
+/* VideoPress Settings Modal style overrides */
+.mce-videopress-field-guid,
+.mce-videopress-field-freedom,
+.mce-videopress-field-flashonly {
+ display: none;
+}
+
+.mce-videopress-checkbox .mce-checkbox {
+ left: 120px !important;
+ width: 100% !important; /* assigning a full width so the label area is clickable */
+}
+
+.mce-videopress-checkbox .mce-label {
+ left: 150px !important;
+}
+
+.mce-videopress-checkbox .mce-label-unit {
+ position: absolute;
+ left: 210px;
+ top: 5px;
+}
+
+.mce-videopress-checkbox i.mce-i-checkbox {
+ background-color: #fff;
+ color: #1e8cbe;
+}
+
+.mce-videopress-checkbox .mce-i-checkbox:before {
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ font: 400 21px/1 dashicons;
+ speak: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ margin: -3px 0 0 -3px;
+ content: "\f147";
+}
+
+.mce-videopress-checkbox .mce-i-checkbox.mce-checked:before {
+ content: "\f147";
+}
+
+div[class*=mce-videopress-field] input[type=number] {
+ width: 70px !important;
+ left: 120px !important;
+}
+
+.mce-videopress-field-w .mce-label,
+.mce-videopress-field-at .mce-label {
+ width: 115px !important;
+ text-align: right;
+}
+
+.mce-videopress-field-unit {
+ position: absolute;
+ left: 210px;
+ top: 5px;
+}
diff --git a/plugins/jetpack/modules/videopress/css/editor.min.css b/plugins/jetpack/modules/videopress/css/editor.min.css
new file mode 100644
index 00000000..f3a010b7
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/css/editor.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.mce-videopress-field-flashonly,.mce-videopress-field-freedom,.mce-videopress-field-guid{display:none}.mce-videopress-checkbox .mce-checkbox{left:120px!important;width:100%!important}.mce-videopress-checkbox .mce-label{left:150px!important}.mce-videopress-checkbox .mce-label-unit{position:absolute;left:210px;top:5px}.mce-videopress-checkbox i.mce-i-checkbox{background-color:#fff;color:#1e8cbe}.mce-videopress-checkbox .mce-i-checkbox:before{display:inline-block;vertical-align:middle;width:16px;font:400 21px/1 dashicons;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin:-3px 0 0 -3px;content:"\f147"}.mce-videopress-checkbox .mce-i-checkbox.mce-checked:before{content:"\f147"}div[class*=mce-videopress-field] input[type=number]{width:70px!important;left:120px!important}.mce-videopress-field-at .mce-label,.mce-videopress-field-w .mce-label{width:115px!important;text-align:right}.mce-videopress-field-unit{position:absolute;left:210px;top:5px} \ No newline at end of file
diff --git a/plugins/jetpack/modules/videopress/css/videopress-editor-style-rtl.css b/plugins/jetpack/modules/videopress/css/videopress-editor-style-rtl.css
new file mode 100644
index 00000000..c3af72cf
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/css/videopress-editor-style-rtl.css
@@ -0,0 +1,22 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+/**
+ * VideoPress styles for Editor
+ */
+.videopress-editor-wrapper {
+ position: relative;
+ max-width: 100%;
+ padding: 56.25% 0 0;
+ height: 0;
+ overflow: hidden;
+}
+.tmpl-videopress_iframe_next iframe {
+ position: absolute;
+ top: 0;
+ right: 0;
+ max-width: 100%;
+ max-height: 100%;
+}
+body.rtl .tmpl-videopress_iframe_next iframe {
+ right: auto;
+ left: 0;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/videopress/css/videopress-editor-style-rtl.min.css b/plugins/jetpack/modules/videopress/css/videopress-editor-style-rtl.min.css
new file mode 100644
index 00000000..53c667e8
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/css/videopress-editor-style-rtl.min.css
@@ -0,0 +1 @@
+.videopress-editor-wrapper{position:relative;max-width:100%;padding:56.25% 0 0;height:0;overflow:hidden}.tmpl-videopress_iframe_next iframe{position:absolute;top:0;right:0;max-width:100%;max-height:100%}body.rtl .tmpl-videopress_iframe_next iframe{right:auto;left:0} \ No newline at end of file
diff --git a/plugins/jetpack/modules/videopress/css/videopress-editor-style.css b/plugins/jetpack/modules/videopress/css/videopress-editor-style.css
new file mode 100644
index 00000000..b2c29c5f
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/css/videopress-editor-style.css
@@ -0,0 +1,21 @@
+/**
+ * VideoPress styles for Editor
+ */
+.videopress-editor-wrapper {
+ position: relative;
+ max-width: 100%;
+ padding: 56.25% 0 0;
+ height: 0;
+ overflow: hidden;
+}
+.tmpl-videopress_iframe_next iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ max-width: 100%;
+ max-height: 100%;
+}
+body.rtl .tmpl-videopress_iframe_next iframe {
+ left: auto;
+ right: 0;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/videopress/css/videopress-editor-style.min.css b/plugins/jetpack/modules/videopress/css/videopress-editor-style.min.css
new file mode 100644
index 00000000..546cfc5c
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/css/videopress-editor-style.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.videopress-editor-wrapper{position:relative;max-width:100%;padding:56.25% 0 0;height:0;overflow:hidden}.tmpl-videopress_iframe_next iframe{position:absolute;top:0;left:0;max-width:100%;max-height:100%}body.rtl .tmpl-videopress_iframe_next iframe{left:auto;right:0} \ No newline at end of file
diff --git a/plugins/jetpack/modules/videopress/editor-media-view.php b/plugins/jetpack/modules/videopress/editor-media-view.php
new file mode 100644
index 00000000..ed65a08c
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/editor-media-view.php
@@ -0,0 +1,236 @@
+<?php
+
+/**
+ * WordPress Shortcode Editor View JS Code
+ */
+function videopress_handle_editor_view_js() {
+ global $content_width;
+ $current_screen = get_current_screen();
+ if ( ! isset( $current_screen->id ) || $current_screen->base !== 'post' ) {
+ return;
+ }
+
+ add_action( 'admin_print_footer_scripts', 'videopress_editor_view_js_templates' );
+
+ wp_enqueue_style( 'videopress-editor-ui', plugins_url( 'css/editor.css', __FILE__ ) );
+ wp_enqueue_script(
+ 'videopress-editor-view',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/videopress/js/editor-view.min.js',
+ 'modules/videopress/js/editor-view.js'
+ ),
+ array( 'wp-util', 'jquery' ),
+ false,
+ true
+ );
+ wp_localize_script(
+ 'videopress-editor-view',
+ 'vpEditorView',
+ array(
+ 'home_url_host' => parse_url( home_url(), PHP_URL_HOST ),
+ 'min_content_width' => VIDEOPRESS_MIN_WIDTH,
+ 'content_width' => $content_width,
+ 'modal_labels' => array(
+ 'title' => esc_html__( 'VideoPress Shortcode', 'jetpack' ),
+ 'guid' => esc_html__( 'Video ID', 'jetpack' ),
+ 'w' => esc_html__( 'Video Width', 'jetpack' ),
+ 'w_unit' => esc_html__( 'pixels', 'jetpack' ),
+ /* Translators: example of usage of this is "Start Video After 10 seconds" */
+ 'at' => esc_html__( 'Start Video After', 'jetpack' ),
+ 'at_unit' => esc_html__( 'seconds', 'jetpack' ),
+ 'hd' => esc_html__( 'High definition on by default', 'jetpack' ),
+ 'permalink' => esc_html__( 'Link the video title to its URL on VideoPress.com', 'jetpack' ),
+ 'autoplay' => esc_html__( 'Autoplay video on page load', 'jetpack' ),
+ 'loop' => esc_html__( 'Loop video playback', 'jetpack' ),
+ 'freedom' => esc_html__( 'Use only Open Source codecs (may degrade performance)', 'jetpack' ),
+ 'flashonly' => esc_html__( 'Use legacy Flash Player (not recommended)', 'jetpack' ),
+ ),
+ )
+ );
+
+ add_editor_style( plugins_url( 'css/videopress-editor-style.css', __FILE__ ) );
+}
+add_action( 'admin_notices', 'videopress_handle_editor_view_js' );
+
+/**
+ * WordPress Editor Views
+ */
+function videopress_editor_view_js_templates() {
+ /**
+ * This template uses the following parameters, and displays the video as an iframe:
+ * - data.guid // The guid of the video.
+ * - data.width // The width of the iframe.
+ * - data.height // The height of the iframe.
+ * - data.urlargs // Arguments serialized into a get string.
+ *
+ * In addition, the calling script will need to ensure that the following
+ * JS file is added to the header of the editor iframe:
+ * - https://s0.wp.com/wp-content/plugins/video/assets/js/next/videopress-iframe.js
+ */
+ ?>
+ <script type="text/html" id="tmpl-videopress_iframe_vnext">
+ <div class="tmpl-videopress_iframe_next" style="max-height:{{ data.height }}px;">
+ <div class="videopress-editor-wrapper" style="padding-top:{{ data.ratio }}%;">
+ <iframe style="display: block;" width="{{ data.width }}" height="{{ data.height }}" src="https://videopress.com/embed/{{ data.guid }}?{{ data.urlargs }}" frameborder='0' allowfullscreen></iframe>
+ </div>
+ </div>
+ </script>
+ <?php
+}
+
+/*************************************************\
+| This is the chunk that handles overriding core |
+| media stuff so VideoPress can display natively. |
+\*/
+
+/**
+ * Media Grid:
+ * Filter out any videopress video posters that we've downloaded,
+ * so that they don't seem to display twice.
+ */
+add_filter( 'ajax_query_attachments_args', 'videopress_ajax_query_attachments_args' );
+function videopress_ajax_query_attachments_args( $args ) {
+ $meta_query = array(
+ array(
+ 'key' => 'videopress_poster_image',
+ 'compare' => 'NOT EXISTS',
+ ),
+ );
+
+ // If there was already a meta query, let's AND it via
+ // nesting it with our new one. No need to specify the
+ // relation, as it defaults to AND.
+ if ( ! empty( $args['meta_query'] ) ) {
+ $meta_query[] = $args['meta_query'];
+ }
+ $args['meta_query'] = $meta_query;
+
+ return $args;
+}
+
+/**
+ * Media List:
+ * Do the same as ^^ but for the list view.
+ */
+add_action( 'pre_get_posts', 'videopress_media_list_table_query' );
+function videopress_media_list_table_query( $query ) {
+
+ if (
+ ! function_exists( 'get_current_screen' )
+ || is_null( get_current_screen() )
+ ) {
+ return;
+ }
+
+ if ( is_admin() && $query->is_main_query() && ( 'upload' === get_current_screen()->id ) ) {
+ $meta_query = array(
+ array(
+ 'key' => 'videopress_poster_image',
+ 'compare' => 'NOT EXISTS',
+ ),
+ );
+
+ if ( $old_meta_query = $query->get( 'meta_query' ) ) {
+ $meta_query[] = $old_meta_query;
+ }
+
+ $query->set( 'meta_query', $meta_query );
+ }
+}
+
+/**
+ * Make sure that any Video that has a VideoPress GUID passes that data back.
+ */
+add_filter( 'wp_prepare_attachment_for_js', 'videopress_prepare_attachment_for_js' );
+function videopress_prepare_attachment_for_js( $post ) {
+ if ( 'video' === $post['type'] ) {
+ $guid = get_post_meta( $post['id'], 'videopress_guid' );
+ if ( $guid ) {
+ $post['videopress_guid'] = $guid;
+ }
+ }
+ return $post;
+}
+
+/**
+ * Wherever the Media Modal is deployed, also deploy our overrides.
+ */
+add_action( 'wp_enqueue_media', 'add_videopress_media_overrides' );
+function add_videopress_media_overrides() {
+ add_action( 'admin_print_footer_scripts', 'videopress_override_media_templates', 11 );
+}
+
+/**
+ * Our video overrides!
+ *
+ * We have a template for the iframe to get injected.
+ */
+function videopress_override_media_templates() {
+ ?>
+ <script type="text/html" id="tmpl-videopress_iframe_vnext">
+ <iframe style="display: block; max-width: 100%;" width="{{ data.width }}" height="{{ data.height }}" src="https://videopress.com/embed/{{ data.guid }}?{{ data.urlargs }}" frameborder='0' allowfullscreen></iframe>
+ </script>
+ <script>
+ (function( media ){
+ // This handles the media library modal attachment details display.
+ if ( 'undefined' !== typeof media.view.Attachment.Details.TwoColumn ) {
+ var TwoColumn = media.view.Attachment.Details.TwoColumn,
+ old_render = TwoColumn.prototype.render,
+ vp_template = wp.template('videopress_iframe_vnext');
+
+ TwoColumn.prototype.render = function() {
+ // Have the original renderer run first.
+ old_render.apply( this, arguments );
+
+ // Now our stuff!
+ if ( 'video' === this.model.get('type') ) {
+ if ( this.model.get('videopress_guid') ) {
+ this.$('.attachment-media-view .thumbnail-video').html( vp_template( {
+ guid : this.model.get('videopress_guid'),
+ width : this.model.get('width'),
+ height : this.model.get('height')
+ }));
+ }
+ }
+ };
+ } else { /* console.log( 'media.view.Attachment.Details.TwoColumn undefined' ); */ }
+
+ // This handles the recreating of the core video shortcode when editing the mce embed.
+ if ( 'undefined' !== typeof media.video ) {
+ media.video.defaults.videopress_guid = '';
+
+ // For some reason, even though we're not currently changing anything, the following proxy
+ // function is necessary to include the above default `videopress_guid` param. ¯\_(ツ)_/¯
+ var old_video_shortcode = media.video.shortcode;
+ media.video.shortcode = function( model ) {
+ // model.videopress_guid = 'FOOBAR';
+ return old_video_shortcode( model );
+ };
+ } else { /* console.log( 'media.video undefined' ); */ }
+
+ })( wp.media );
+ </script>
+ <?php
+}
+
+/**
+ * Properly inject VideoPress data into Core shortcodes, and
+ * generate videopress shortcodes for purely remote videos.
+ */
+add_filter( 'media_send_to_editor', 'videopress_media_send_to_editor', 10, 3 );
+function videopress_media_send_to_editor( $html, $id, $attachment ) {
+ $videopress_guid = get_post_meta( $id, 'videopress_guid', true );
+ if ( $videopress_guid && videopress_is_valid_guid( $videopress_guid ) ) {
+ if ( '[video ' === substr( $html, 0, 7 ) ) {
+ $html = sprintf( '[videopress %1$s]', esc_attr( $videopress_guid ) );
+
+ } elseif ( '<a href=' === substr( $html, 0, 8 ) ) {
+ // We got here because `wp_attachment_is()` returned false for
+ // video, because there isn't a local copy of the file.
+ $html = sprintf( '[videopress %1$s]', esc_attr( $videopress_guid ) );
+ }
+ } elseif ( videopress_is_attachment_without_guid( $id ) ) {
+ $html = sprintf( '[videopress postid=%d]', $id );
+ }
+ return $html;
+}
diff --git a/plugins/jetpack/modules/videopress/js/editor-view.js b/plugins/jetpack/modules/videopress/js/editor-view.js
new file mode 100644
index 00000000..2b9c2873
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/js/editor-view.js
@@ -0,0 +1,303 @@
+/* global tinyMCE, vpEditorView */
+( function( $, wp, vpEditorView ) {
+ wp.mce = wp.mce || {};
+ if ( 'undefined' === typeof wp.mce.views ) {
+ return;
+ }
+ wp.mce.videopress_wp_view_renderer = {
+ shortcode_string: 'videopress',
+ shortcode_data: {},
+ defaults: {
+ w: '',
+ at: '',
+ permalink: true,
+ hd: false,
+ loop: false,
+ freedom: false,
+ autoplay: false,
+ flashonly: false,
+ },
+ coerce: wp.media.coerce,
+ template: wp.template( 'videopress_iframe_vnext' ),
+ getContent: function() {
+ var urlargs = 'for=' + encodeURIComponent( vpEditorView.home_url_host ),
+ named = this.shortcode.attrs.named,
+ options,
+ key,
+ width;
+
+ for ( key in named ) {
+ switch ( key ) {
+ case 'at':
+ if ( parseInt( named[ key ], 10 ) ) {
+ urlargs += '&' + key + '=' + parseInt( named[ key ], 10 );
+ } // Else omit, as it's the default.
+ break;
+ case 'permalink':
+ if ( 'false' === named[ key ] ) {
+ urlargs += '&' + key + '=0';
+ } // Else omit, as it's the default.
+ break;
+ case 'hd':
+ case 'loop':
+ case 'autoplay':
+ if ( 'true' === named[ key ] ) {
+ urlargs += '&' + key + '=1';
+ } // Else omit, as it's the default.
+ break;
+ default:
+ // Unknown parameters? Ditch it!
+ break;
+ }
+ }
+
+ options = {
+ width: vpEditorView.content_width,
+ height: vpEditorView.content_width * 0.5625,
+ guid: this.shortcode.attrs.numeric[ 0 ],
+ urlargs: urlargs,
+ };
+
+ if ( typeof named.w !== 'undefined' ) {
+ width = parseInt( named.w, 10 );
+ if ( width >= vpEditorView.min_content_width && width < vpEditorView.content_width ) {
+ options.width = width;
+ options.height = parseInt( width * 0.5625, 10 );
+ }
+ }
+
+ options.ratio = 100 * ( options.height / options.width );
+
+ return this.template( options );
+ },
+ edit: function( data ) {
+ var shortcode_data = wp.shortcode.next( this.shortcode_string, data ),
+ named = shortcode_data.shortcode.attrs.named,
+ editor = tinyMCE.activeEditor,
+ renderer = this,
+ oldRenderFormItem = tinyMCE.ui.FormItem.prototype.renderHtml;
+
+ /**
+ * Override TextBox renderHtml to support html5 attrs.
+ * @link https://github.com/tinymce/tinymce/pull/2784
+ *
+ * @returns {string}
+ */
+ tinyMCE.ui.TextBox.prototype.renderHtml = function() {
+ var self = this,
+ settings = self.settings,
+ element = document.createElement( settings.multiline ? 'textarea' : 'input' ),
+ extraAttrs = [
+ 'rows',
+ 'spellcheck',
+ 'maxLength',
+ 'size',
+ 'readonly',
+ 'min',
+ 'max',
+ 'step',
+ 'list',
+ 'pattern',
+ 'placeholder',
+ 'required',
+ 'multiple',
+ ],
+ i,
+ key;
+
+ for ( i = 0; i < extraAttrs.length; i++ ) {
+ key = extraAttrs[ i ];
+ if ( typeof settings[ key ] !== 'undefined' ) {
+ element.setAttribute( key, settings[ key ] );
+ }
+ }
+
+ if ( settings.multiline ) {
+ element.innerText = self.state.get( 'value' );
+ } else {
+ element.setAttribute( 'type', settings.subtype ? settings.subtype : 'text' );
+ element.setAttribute( 'value', self.state.get( 'value' ) );
+ }
+
+ element.id = self._id;
+ element.className = self.classes;
+ element.setAttribute( 'hidefocus', 1 );
+ if ( self.disabled() ) {
+ element.disabled = true;
+ }
+
+ return element.outerHTML;
+ };
+
+ tinyMCE.ui.FormItem.prototype.renderHtml = function() {
+ _.each(
+ vpEditorView.modal_labels,
+ function( value, key ) {
+ if ( value === this.settings.items.text ) {
+ this.classes.add( 'videopress-field-' + key );
+ }
+ },
+ this
+ );
+
+ if (
+ _.contains(
+ [
+ vpEditorView.modal_labels.hd,
+ vpEditorView.modal_labels.permalink,
+ vpEditorView.modal_labels.autoplay,
+ vpEditorView.modal_labels.loop,
+ vpEditorView.modal_labels.freedom,
+ vpEditorView.modal_labels.flashonly,
+ ],
+ this.settings.items.text
+ )
+ ) {
+ this.classes.add( 'videopress-checkbox' );
+ }
+ return oldRenderFormItem.call( this );
+ };
+
+ /**
+ * Populate the defaults.
+ */
+ _.each(
+ this.defaults,
+ function( value, key ) {
+ named[ key ] = this.coerce( named, key );
+ },
+ this
+ );
+
+ /**
+ * Declare the fields that will show in the popup when editing the shortcode.
+ */
+ editor.windowManager.open( {
+ title: vpEditorView.modal_labels.title,
+ id: 'videopress-shortcode-settings-modal',
+ width: 520,
+ height: 240,
+ body: [
+ {
+ type: 'textbox',
+ disabled: true,
+ name: 'guid',
+ label: vpEditorView.modal_labels.guid,
+ value: shortcode_data.shortcode.attrs.numeric[ 0 ],
+ },
+ {
+ type: 'textbox',
+ subtype: 'number',
+ min: vpEditorView.min_content_width, // The `min` may supported be in the future. https://github.com/tinymce/tinymce/pull/2784
+ name: 'w',
+ label: vpEditorView.modal_labels.w,
+ value: named.w,
+ },
+ {
+ type: 'textbox',
+ subtype: 'number',
+ min: 0, // The `min` may supported be in the future. https://github.com/tinymce/tinymce/pull/2784
+ name: 'at',
+ label: vpEditorView.modal_labels.at,
+ value: named.at,
+ },
+ {
+ type: 'checkbox',
+ name: 'hd',
+ label: vpEditorView.modal_labels.hd,
+ checked: named.hd,
+ },
+ {
+ type: 'checkbox',
+ name: 'permalink',
+ label: vpEditorView.modal_labels.permalink,
+ checked: named.permalink,
+ },
+ {
+ type: 'checkbox',
+ name: 'autoplay',
+ label: vpEditorView.modal_labels.autoplay,
+ checked: named.autoplay,
+ },
+ {
+ type: 'checkbox',
+ name: 'loop',
+ label: vpEditorView.modal_labels.loop,
+ checked: named.loop,
+ },
+ {
+ type: 'checkbox',
+ name: 'freedom',
+ label: vpEditorView.modal_labels.freedom,
+ checked: named.freedom,
+ },
+ {
+ type: 'checkbox',
+ name: 'flashonly',
+ label: vpEditorView.modal_labels.flashonly,
+ checked: named.flashonly,
+ },
+ ],
+ onsubmit: function( e ) {
+ var args = {
+ tag: renderer.shortcode_string,
+ type: 'single',
+ attrs: {
+ named: _.pick( e.data, _.keys( renderer.defaults ) ),
+ numeric: [ e.data.guid ],
+ },
+ };
+
+ if ( '0' === args.attrs.named.at ) {
+ args.attrs.named.at = '';
+ }
+
+ _.each(
+ renderer.defaults,
+ function( value, key ) {
+ args.attrs.named[ key ] = this.coerce( args.attrs.named, key );
+
+ if ( value === args.attrs.named[ key ] ) {
+ delete args.attrs.named[ key ];
+ }
+ },
+ renderer
+ );
+
+ editor.insertContent( wp.shortcode.string( args ) );
+ },
+ onopen: function( e ) {
+ var prefix = 'mce-videopress-field-';
+ _.each( [ 'w', 'at' ], function( value ) {
+ e.target.$el
+ .find( '.' + prefix + value + ' .mce-container-body' )
+ .append(
+ '<span class="' +
+ prefix +
+ 'unit ' +
+ prefix +
+ 'unit-' +
+ value +
+ '">' +
+ vpEditorView.modal_labels[ value + '_unit' ]
+ );
+ } );
+ $( 'body' ).addClass( 'modal-open' );
+ },
+ onclose: function() {
+ $( 'body' ).removeClass( 'modal-open' );
+ },
+ } );
+
+ // Set it back to its original renderer.
+ tinyMCE.ui.FormItem.prototype.renderHtml = oldRenderFormItem;
+ },
+ };
+ wp.mce.views.register( 'videopress', wp.mce.videopress_wp_view_renderer );
+
+ // Extend the videopress one to also handle `wpvideo` instances.
+ wp.mce.wpvideo_wp_view_renderer = _.extend( {}, wp.mce.videopress_wp_view_renderer, {
+ shortcode_string: 'wpvideo',
+ } );
+ wp.mce.views.register( 'wpvideo', wp.mce.wpvideo_wp_view_renderer );
+} )( jQuery, wp, vpEditorView );
diff --git a/plugins/jetpack/modules/videopress/js/media-video-widget-extensions.js b/plugins/jetpack/modules/videopress/js/media-video-widget-extensions.js
new file mode 100644
index 00000000..d7d9845c
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/js/media-video-widget-extensions.js
@@ -0,0 +1,52 @@
+window.wp = window.wp || {};
+
+( function( wp ) {
+ if ( wp.mediaWidgets ) {
+ // Over-ride core media_video#mapMediaToModelProps to set the url based upon videopress_guid if it exists.
+ wp.mediaWidgets.controlConstructors.media_video.prototype.mapMediaToModelProps = ( function(
+ originalMapMediaToModelProps
+ ) {
+ return function( mediaFrameProps ) {
+ var newProps, originalProps, videoPressGuid;
+ originalProps = originalMapMediaToModelProps.call( this, mediaFrameProps );
+ newProps = _.extend( {}, originalProps );
+
+ // API response on new media will have the guid at videopress.guid.
+ if ( mediaFrameProps.videopress && mediaFrameProps.videopress.guid ) {
+ videoPressGuid = mediaFrameProps.videopress.guid;
+ }
+
+ // Selecting an existing VideoPress file will have the guid at .videopress_guid[ 0 ].
+ if (
+ ! videoPressGuid &&
+ mediaFrameProps.videopress_guid &&
+ mediaFrameProps.videopress_guid.length
+ ) {
+ videoPressGuid = mediaFrameProps.videopress_guid[ 0 ];
+ }
+
+ if ( videoPressGuid ) {
+ newProps = _.extend( {}, originalProps, {
+ url: 'https://videopress.com/v/' + videoPressGuid,
+ attachment_id: 0,
+ } );
+ }
+ return newProps;
+ };
+ } )( wp.mediaWidgets.controlConstructors.media_video.prototype.mapMediaToModelProps );
+
+ // Over-ride core media_video#isHostedVideo() to add support for videopress oembed urls.
+ wp.mediaWidgets.controlConstructors.media_video.prototype.isHostedVideo = ( function(
+ originalIsHostedVideo
+ ) {
+ return function( url ) {
+ var parsedUrl = document.createElement( 'a' );
+ parsedUrl.href = url;
+ if ( 'videopress.com' === parsedUrl.hostname ) {
+ return true;
+ }
+ return originalIsHostedVideo.call( this, url );
+ };
+ } )( wp.mediaWidgets.controlConstructors.media_video.prototype.isHostedVideo );
+ }
+} )( window.wp );
diff --git a/plugins/jetpack/modules/videopress/js/videopress-plupload.js b/plugins/jetpack/modules/videopress/js/videopress-plupload.js
new file mode 100644
index 00000000..0bec3c50
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/js/videopress-plupload.js
@@ -0,0 +1,483 @@
+/* global pluploadL10n, plupload, _wpPluploadSettings, JSON */
+
+window.wp = window.wp || {};
+
+( function( exports, $ ) {
+ var Uploader, vp;
+
+ if ( typeof _wpPluploadSettings === 'undefined' ) {
+ return;
+ }
+
+ /**
+ * A WordPress uploader.
+ *
+ * The Plupload library provides cross-browser uploader UI integration.
+ * This object bridges the Plupload API to integrate uploads into the
+ * WordPress back end and the WordPress media experience.
+ *
+ * @param {object} options The options passed to the new plupload instance.
+ * @param {object} options.container The id of uploader container.
+ * @param {object} options.browser The id of button to trigger the file select.
+ * @param {object} options.dropzone The id of file drop target.
+ * @param {object} options.plupload An object of parameters to pass to the plupload instance.
+ * @param {object} options.params An object of parameters to pass to $_POST when uploading the file.
+ * Extends this.plupload.multipart_params under the hood.
+ */
+ Uploader = function( options ) {
+ var self = this,
+ isIE =
+ navigator.userAgent.indexOf( 'Trident/' ) !== -1 ||
+ navigator.userAgent.indexOf( 'MSIE ' ) !== -1,
+ elements = {
+ container: 'container',
+ browser: 'browse_button',
+ dropzone: 'drop_element',
+ },
+ key,
+ error;
+
+ this.supports = {
+ upload: Uploader.browser.supported,
+ };
+
+ this.supported = this.supports.upload;
+
+ if ( ! this.supported ) {
+ return;
+ }
+
+ // Arguments to send to pluplad.Uploader().
+ // Use deep extend to ensure that multipart_params and other objects are cloned.
+ this.plupload = $.extend( true, { multipart_params: {} }, Uploader.defaults );
+ this.container = document.body; // Set default container.
+
+ // Extend the instance with options.
+ //
+ // Use deep extend to allow options.plupload to override individual
+ // default plupload keys.
+ $.extend( true, this, options );
+
+ // Proxy all methods so this always refers to the current instance.
+ for ( key in this ) {
+ if ( $.isFunction( this[ key ] ) ) {
+ this[ key ] = $.proxy( this[ key ], this );
+ }
+ }
+
+ // Ensure all elements are jQuery elements and have id attributes,
+ // then set the proper plupload arguments to the ids.
+ for ( key in elements ) {
+ if ( ! this[ key ] ) {
+ continue;
+ }
+
+ this[ key ] = $( this[ key ] ).first();
+
+ if ( ! this[ key ].length ) {
+ delete this[ key ];
+ continue;
+ }
+
+ if ( ! this[ key ].prop( 'id' ) ) {
+ this[ key ].prop( 'id', '__wp-uploader-id-' + Uploader.uuid++ );
+ }
+
+ this.plupload[ elements[ key ] ] = this[ key ].prop( 'id' );
+ }
+
+ // If the uploader has neither a browse button nor a dropzone, bail.
+ if (
+ ! ( this.browser && this.browser.length ) &&
+ ! ( this.dropzone && this.dropzone.length )
+ ) {
+ return;
+ }
+
+ // Make sure flash sends cookies (seems in IE it does without switching to urlstream mode)
+ if (
+ ! isIE &&
+ 'flash' === plupload.predictRuntime( this.plupload ) &&
+ ( ! this.plupload.required_features ||
+ ! this.plupload.required_features.hasOwnProperty( 'send_binary_string' ) )
+ ) {
+ this.plupload.required_features = this.plupload.required_features || {};
+ this.plupload.required_features.send_binary_string = true;
+ }
+
+ // Initialize the plupload instance.
+ this.uploader = new plupload.Uploader( this.plupload );
+ delete this.plupload;
+
+ // Set default params and remove this.params alias.
+ this.param( this.params || {} );
+ delete this.params;
+
+ // Make sure that the VideoPress object is available
+ if ( typeof exports.VideoPress !== 'undefined' ) {
+ vp = exports.VideoPress;
+ } else {
+ window.console &&
+ window.console.error( 'The VideoPress object was not loaded. Errors may occur.' );
+ }
+
+ /**
+ * Custom error callback.
+ *
+ * Add a new error to the errors collection, so other modules can track
+ * and display errors. @see wp.Uploader.errors.
+ *
+ * @param {string} message
+ * @param {object} data
+ * @param {plupload.File} file File that was uploaded.
+ */
+ error = function( message, data, file ) {
+ if ( file.attachment ) {
+ file.attachment.destroy();
+ }
+
+ Uploader.errors.unshift( {
+ message: message || pluploadL10n.default_error,
+ data: data,
+ file: file,
+ } );
+
+ self.error( message, data, file );
+ };
+
+ /**
+ * After the Uploader has been initialized, initialize some behaviors for the dropzone.
+ *
+ * @param {plupload.Uploader} uploader Uploader instance.
+ */
+ this.uploader.bind( 'init', function( uploader ) {
+ var timer,
+ active,
+ dragdrop,
+ dropzone = self.dropzone;
+
+ dragdrop = self.supports.dragdrop = uploader.features.dragdrop && ! Uploader.browser.mobile;
+
+ // Generate drag/drop helper classes.
+ if ( ! dropzone ) {
+ return;
+ }
+
+ dropzone.toggleClass( 'supports-drag-drop', !! dragdrop );
+
+ if ( ! dragdrop ) {
+ return dropzone.unbind( '.wp-uploader' );
+ }
+
+ // 'dragenter' doesn't fire correctly, simulate it with a limited 'dragover'.
+ dropzone.bind( 'dragover.wp-uploader', function() {
+ if ( timer ) {
+ clearTimeout( timer );
+ }
+
+ if ( active ) {
+ return;
+ }
+
+ dropzone.trigger( 'dropzone:enter' ).addClass( 'drag-over' );
+ active = true;
+ } );
+
+ dropzone.bind( 'dragleave.wp-uploader, drop.wp-uploader', function() {
+ // Using an instant timer prevents the drag-over class from
+ // being quickly removed and re-added when elements inside the
+ // dropzone are repositioned.
+ //
+ // @see https://core.trac.wordpress.org/ticket/21705
+ timer = setTimeout( function() {
+ active = false;
+ dropzone.trigger( 'dropzone:leave' ).removeClass( 'drag-over' );
+ }, 0 );
+ } );
+
+ self.ready = true;
+ $( self ).trigger( 'uploader:ready' );
+ } );
+
+ this.uploader.bind( 'postinit', function( up ) {
+ up.refresh();
+ self.init();
+ } );
+
+ this.uploader.init();
+
+ if ( this.browser ) {
+ this.browser.on( 'mouseenter', this.refresh );
+ } else {
+ this.uploader.disableBrowse( true );
+ // If HTML5 mode, hide the auto-created file container.
+ $( '#' + this.uploader.id + '_html5_container' ).hide();
+ }
+
+ /**
+ * After files were filtered and added to the queue, create a model for each.
+ *
+ * @event FilesAdded
+ * @param {plupload.Uploader} uploader Uploader instance.
+ * @param {Array} files Array of file objects that were added to queue by the user.
+ */
+ this.uploader.bind( 'FilesAdded', function( up, files ) {
+ _.each( files, function( file ) {
+ var attributes, image;
+
+ // Ignore failed uploads.
+ if ( plupload.FAILED === file.status ) {
+ return;
+ }
+
+ // Generate attributes for a new `Attachment` model.
+ attributes = _.extend(
+ {
+ file: file,
+ uploading: true,
+ date: new Date(),
+ filename: file.name,
+ menuOrder: 0,
+ uploadedTo: wp.media.model.settings.post.id,
+ },
+ _.pick( file, 'loaded', 'size', 'percent' )
+ );
+
+ // Handle early mime type scanning for images.
+ image = /(?:jpe?g|png|gif)$/i.exec( file.name );
+
+ // For images set the model's type and subtype attributes.
+ if ( image ) {
+ attributes.type = 'image';
+
+ // `jpeg`, `png` and `gif` are valid subtypes.
+ // `jpg` is not, so map it to `jpeg`.
+ attributes.subtype = 'jpg' === image[ 0 ] ? 'jpeg' : image[ 0 ];
+ }
+
+ // Create a model for the attachment, and add it to the Upload queue collection
+ // so listeners to the upload queue can track and display upload progress.
+ file.attachment = wp.media.model.Attachment.create( attributes );
+ Uploader.queue.add( file.attachment );
+
+ self.added( file.attachment );
+ } );
+
+ up.refresh();
+ up.start();
+ } );
+
+ this.uploader.bind( 'UploadProgress', function( up, file ) {
+ file.attachment.set( _.pick( file, 'loaded', 'percent' ) );
+ self.progress( file.attachment );
+ } );
+
+ /**
+ * After a file is successfully uploaded, update its model.
+ *
+ * @param {plupload.Uploader} uploader Uploader instance.
+ * @param {plupload.File} file File that was uploaded.
+ * @param {Object} response Object with response properties.
+ * @return {mixed}
+ */
+ this.uploader.bind( 'FileUploaded', function( up, file, response ) {
+ var complete;
+
+ try {
+ response = JSON.parse( response.response );
+ } catch ( e ) {
+ return error( pluploadL10n.default_error, e, file );
+ }
+
+ if ( typeof response.media !== 'undefined' ) {
+ response = vp.handleRestApiResponse( response, file );
+ } else {
+ response = vp.handleStandardResponse( response, file );
+ }
+
+ _.each( [ 'file', 'loaded', 'size', 'percent' ], function( key ) {
+ file.attachment.unset( key );
+ } );
+
+ file.attachment.set( _.extend( response.data, { uploading: false } ) );
+ wp.media.model.Attachment.get( response.data.id, file.attachment );
+
+ complete = Uploader.queue.all( function( attachment ) {
+ return ! attachment.get( 'uploading' );
+ } );
+
+ if ( complete ) {
+ vp && vp.resetToOriginalOptions( up );
+ Uploader.queue.reset();
+ }
+
+ self.success( file.attachment );
+ } );
+
+ /**
+ * When plupload surfaces an error, send it to the error handler.
+ *
+ * @param {plupload.Uploader} uploader Uploader instance.
+ * @param {Object} error Contains code, message and sometimes file and other details.
+ */
+ this.uploader.bind( 'Error', function( up, pluploadError ) {
+ var message = pluploadL10n.default_error,
+ key;
+
+ // Check for plupload errors.
+ for ( key in Uploader.errorMap ) {
+ if ( pluploadError.code === plupload[ key ] ) {
+ message = Uploader.errorMap[ key ];
+
+ if ( _.isFunction( message ) ) {
+ message = message( pluploadError.file, pluploadError );
+ }
+
+ break;
+ }
+ }
+
+ error( message, pluploadError, pluploadError.file );
+ vp && vp.resetToOriginalOptions( up );
+ up.refresh();
+ } );
+
+ /**
+ * Add in a way for the uploader to reset itself when uploads are complete.
+ */
+ this.uploader.bind( 'UploadComplete', function( up ) {
+ vp && vp.resetToOriginalOptions( up );
+ } );
+
+ /**
+ * Before we upload, check to see if this file is a videopress upload, if so, set new options and save the old ones.
+ */
+ this.uploader.bind( 'BeforeUpload', function( up, file ) {
+ if ( typeof file.videopress !== 'undefined' ) {
+ vp.originalOptions.url = up.getOption( 'url' );
+ vp.originalOptions.multipart_params = up.getOption( 'multipart_params' );
+ vp.originalOptions.file_data_name = up.getOption( 'file_data_name' );
+
+ up.setOption( 'file_data_name', 'media[]' );
+ up.setOption( 'url', file.videopress.upload_action_url );
+ up.setOption( 'headers', {
+ Authorization:
+ 'X_UPLOAD_TOKEN token="' +
+ file.videopress.upload_token +
+ '" blog_id="' +
+ file.videopress.upload_blog_id +
+ '"',
+ } );
+ }
+ } );
+ };
+
+ // Adds the 'defaults' and 'browser' properties.
+ $.extend( Uploader, _wpPluploadSettings );
+
+ Uploader.uuid = 0;
+
+ // Map Plupload error codes to user friendly error messages.
+ Uploader.errorMap = {
+ FAILED: pluploadL10n.upload_failed,
+ FILE_EXTENSION_ERROR: pluploadL10n.invalid_filetype,
+ IMAGE_FORMAT_ERROR: pluploadL10n.not_an_image,
+ IMAGE_MEMORY_ERROR: pluploadL10n.image_memory_exceeded,
+ IMAGE_DIMENSIONS_ERROR: pluploadL10n.image_dimensions_exceeded,
+ GENERIC_ERROR: pluploadL10n.upload_failed,
+ IO_ERROR: pluploadL10n.io_error,
+ HTTP_ERROR: pluploadL10n.http_error,
+ SECURITY_ERROR: pluploadL10n.security_error,
+
+ FILE_SIZE_ERROR: function( file ) {
+ return pluploadL10n.file_exceeds_size_limit.replace( '%s', file.name );
+ },
+ };
+
+ $.extend( Uploader.prototype, {
+ /**
+ * Acts as a shortcut to extending the uploader's multipart_params object.
+ *
+ * param( key )
+ * Returns the value of the key.
+ *
+ * param( key, value )
+ * Sets the value of a key.
+ *
+ * param( map )
+ * Sets values for a map of data.
+ */
+ param: function( key, value ) {
+ if ( arguments.length === 1 && typeof key === 'string' ) {
+ return this.uploader.settings.multipart_params[ key ];
+ }
+
+ if ( arguments.length > 1 ) {
+ this.uploader.settings.multipart_params[ key ] = value;
+ } else {
+ $.extend( this.uploader.settings.multipart_params, key );
+ }
+ },
+
+ /**
+ * Make a few internal event callbacks available on the wp.Uploader object
+ * to change the Uploader internals if absolutely necessary.
+ */
+ init: function() {},
+ error: function() {},
+ success: function() {},
+ added: function() {},
+ progress: function() {},
+ complete: function() {},
+ refresh: function() {
+ var node, attached, container, id;
+
+ if ( this.browser ) {
+ node = this.browser[ 0 ];
+
+ // Check if the browser node is in the DOM.
+ while ( node ) {
+ if ( node === document.body ) {
+ attached = true;
+ break;
+ }
+ node = node.parentNode;
+ }
+
+ // If the browser node is not attached to the DOM, use a
+ // temporary container to house it, as the browser button
+ // shims require the button to exist in the DOM at all times.
+ if ( ! attached ) {
+ id = 'wp-uploader-browser-' + this.uploader.id;
+
+ container = $( '#' + id );
+ if ( ! container.length ) {
+ container = $( '<div class="wp-uploader-browser" />' )
+ .css( {
+ position: 'fixed',
+ top: '-1000px',
+ left: '-1000px',
+ height: 0,
+ width: 0,
+ } )
+ .attr( 'id', 'wp-uploader-browser-' + this.uploader.id )
+ .appendTo( 'body' );
+ }
+
+ container.append( this.browser );
+ }
+ }
+
+ this.uploader.refresh();
+ },
+ } );
+
+ // Create a collection of attachments in the upload queue,
+ // so that other modules can track and display upload progress.
+ Uploader.queue = new wp.media.model.Attachments( [], { query: false } );
+
+ // Create a collection to collect errors incurred while attempting upload.
+ Uploader.errors = new Backbone.Collection();
+
+ exports.Uploader = Uploader;
+} )( wp, jQuery );
diff --git a/plugins/jetpack/modules/videopress/js/videopress-uploader.js b/plugins/jetpack/modules/videopress/js/videopress-uploader.js
new file mode 100644
index 00000000..de5f9a67
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/js/videopress-uploader.js
@@ -0,0 +1,157 @@
+/* globals plupload, pluploadL10n, error */
+window.wp = window.wp || {};
+
+( function( wp ) {
+ var VideoPress = {
+ originalOptions: {},
+
+ /**
+ * This is the standard uploader response handler.
+ */
+ handleStandardResponse: function( response, file ) {
+ if ( ! _.isObject( response ) || _.isUndefined( response.success ) ) {
+ return error( pluploadL10n.default_error, null, file );
+ } else if ( ! response.success ) {
+ return error( response.data && response.data.message, response.data, file );
+ }
+
+ return response;
+ },
+
+ /**
+ * Handle response from the WPCOM Rest API.
+ */
+ handleRestApiResponse: function( response, file ) {
+ if ( response.media.length !== 1 ) {
+ return error( pluploadL10n.default_error, null, file );
+ }
+
+ var media = response.media[ 0 ],
+ mimeParts = media.mime_type.split( '/' ),
+ data = {
+ alt: '',
+ author: media.author_ID || 0,
+ authorName: '',
+ caption: '',
+ compat: { item: '', meta: '' },
+ date: media.date || '',
+ dateFormatted: media.date || '',
+ description: media.description || '',
+ editLink: '',
+ filename: media.file || '',
+ filesizeHumanReadable: '',
+ filesizeInBytes: '',
+ height: media.height,
+ icon: media.icon || '',
+ id: media.ID || '',
+ link: media.URL || '',
+ menuOrder: 0,
+ meta: false,
+ mime: media.mime_type || '',
+ modified: 0,
+ name: '',
+ nonces: { update: '', delete: '', edit: '' },
+ orientation: '',
+ sizes: undefined,
+ status: '',
+ subtype: mimeParts[ 1 ] || '',
+ title: media.title || '',
+ type: mimeParts[ 0 ] || '',
+ uploadedTo: 1,
+ uploadedToLink: '',
+ uploadedToTitle: '',
+ url: media.URL || '',
+ width: media.width,
+ success: '',
+ videopress: {
+ guid: media.videopress_guid || null,
+ processing_done: media.videopress_processing_done || false,
+ },
+ };
+
+ response.data = data;
+
+ return response;
+ },
+
+ /**
+ * Make sure that all of the original variables have been reset, so the uploader
+ * doesn't try to go to VideoPress again next time.
+ *
+ * @param up
+ */
+ resetToOriginalOptions: function( up ) {
+ if ( typeof VideoPress.originalOptions.url !== 'undefined' ) {
+ up.setOption( 'url', VideoPress.originalOptions.url );
+ delete VideoPress.originalOptions.url;
+ }
+
+ if ( typeof VideoPress.originalOptions.multipart_params !== 'undefined' ) {
+ up.setOption( 'multipart_params', VideoPress.originalOptions.multipart_params );
+ delete VideoPress.originalOptions.multipart_params;
+ }
+
+ if ( typeof VideoPress.originalOptions.file_data_name !== 'undefined' ) {
+ up.setOption( 'file_data_name', VideoPress.originalOptions.file_data_name );
+ delete VideoPress.originalOptions.file_data_name;
+ }
+ },
+ };
+
+ if ( typeof wp.Uploader !== 'undefined' ) {
+ var media = wp.media;
+
+ /**
+ * A plupload code specifically for videopress failures.
+ *
+ * @type {string}
+ */
+ plupload.VIDEOPRESS_TOKEN_FAILURE = 'VP_TOKEN_FAILURE';
+
+ /**
+ * Adds a filter that checks all files to see if they are videopress files and if they are
+ * it will download extra metadata for them.
+ */
+ plupload.addFileFilter( 'videopress_check_uploads', function( maxSize, file, cb ) {
+ var mimeParts = file.type.split( '/' );
+ var self = this;
+
+ if ( mimeParts[ 0 ] === 'video' ) {
+ media
+ .ajax( 'videopress-get-upload-token', { async: false, data: { filename: file.name } } )
+ .done( function( response ) {
+ file.videopress = response;
+ cb( true );
+ } )
+ .fail( function( response ) {
+ self.trigger( 'Error', {
+ code: plupload.VIDEOPRESS_TOKEN_FAILURE,
+ message: plupload.translate(
+ 'Could not get the VideoPress token needed for uploading'
+ ),
+ file: file,
+ response: response,
+ } );
+ cb( false );
+ } );
+ } else {
+ // Handles the normal max_file_size functionality.
+ var undef;
+
+ // Invalid file size
+ if ( file.size !== undef && maxSize && file.size > maxSize ) {
+ this.trigger( 'Error', {
+ code: plupload.FILE_SIZE_ERROR,
+ message: plupload.translate( 'File size error.' ),
+ file: file,
+ } );
+ cb( false );
+ } else {
+ cb( true );
+ }
+ }
+ } );
+ }
+
+ wp.VideoPress = VideoPress;
+} )( window.wp );
diff --git a/plugins/jetpack/modules/videopress/shortcode.php b/plugins/jetpack/modules/videopress/shortcode.php
new file mode 100644
index 00000000..21b163ef
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/shortcode.php
@@ -0,0 +1,248 @@
+<?php
+
+/**
+ * VideoPress Shortcode Handler
+ *
+ * This file may or may not be included from the Jetpack VideoPress module.
+ */
+
+class VideoPress_Shortcode {
+ /** @var VideoPress_Shortcode */
+ protected static $instance;
+
+ protected function __construct() {
+
+ // By explicitly declaring the provider here, we can speed things up by not relying on oEmbed discovery.
+ wp_oembed_add_provider( '#^https?://videopress.com/v/.*#', 'http://public-api.wordpress.com/oembed/1.0/', true );
+
+ add_shortcode( 'videopress', array( $this, 'shortcode_callback' ) );
+ add_shortcode( 'wpvideo', array( $this, 'shortcode_callback' ) );
+
+ add_filter( 'wp_video_shortcode_override', array( $this, 'video_shortcode_override' ), 10, 4 );
+
+ add_filter( 'oembed_fetch_url', array( $this, 'add_oembed_for_parameter' ) );
+
+ $this->add_video_embed_hander();
+ }
+
+ /**
+ * @return VideoPress_Shortcode
+ */
+ public static function initialize() {
+ if ( ! isset( self::$instance ) ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Translate a 'videopress' or 'wpvideo' shortcode and arguments into a video player display.
+ *
+ * Expected input formats:
+ *
+ * [videopress OcobLTqC]
+ * [wpvideo OcobLTqC]
+ *
+ * @link http://codex.wordpress.org/Shortcode_API Shortcode API
+ * @param array $attr shortcode attributes
+ * @return string HTML markup or blank string on fail
+ */
+ public function shortcode_callback( $attr ) {
+ global $content_width;
+
+ /**
+ * We only accept GUIDs as a first unnamed argument.
+ */
+ $guid = isset( $attr[0] ) ? $attr[0] : null;
+
+ if ( isset( $attr['postid'] ) ) {
+ $guid = get_post_meta( $attr['postid'], 'videopress_guid', true );
+ }
+
+ /**
+ * Make sure the GUID passed in matches how actual GUIDs are formatted.
+ */
+ if ( ! videopress_is_valid_guid( $guid ) ) {
+ return '';
+ }
+
+ /**
+ * Set the defaults
+ */
+ $defaults = array(
+ 'w' => 0, // Width of the video player, in pixels
+ 'at' => 0, // How many seconds in to initially seek to
+ 'hd' => true, // Whether to display a high definition version
+ 'loop' => false, // Whether to loop the video repeatedly
+ 'freedom' => false, // Whether to use only free/libre codecs
+ 'autoplay' => false, // Whether to autoplay the video on load
+ 'permalink' => true, // Whether to display the permalink to the video
+ 'flashonly' => false, // Whether to support the Flash player exclusively
+ 'defaultlangcode' => false, // Default language code
+ );
+
+ $attr = shortcode_atts( $defaults, $attr, 'videopress' );
+
+ /**
+ * Cast the attributes, post-input.
+ */
+ $attr['width'] = absint( $attr['w'] );
+ $attr['hd'] = (bool) $attr['hd'];
+ $attr['freedom'] = (bool) $attr['freedom'];
+
+ /**
+ * If the provided width is less than the minimum allowed
+ * width, or greater than `$content_width` ignore.
+ */
+ if ( $attr['width'] < VIDEOPRESS_MIN_WIDTH ) {
+ $attr['width'] = 0;
+ } elseif ( isset( $content_width ) && $content_width > VIDEOPRESS_MIN_WIDTH && $attr['width'] > $content_width ) {
+ $attr['width'] = 0;
+ }
+
+ /**
+ * If there was an invalid or unspecified width, set the width equal to the theme's `$content_width`.
+ */
+ if ( 0 === $attr['width'] && isset( $content_width ) && $content_width >= VIDEOPRESS_MIN_WIDTH ) {
+ $attr['width'] = $content_width;
+ }
+
+ /**
+ * If the width isn't an even number, reduce it by one (making it even).
+ */
+ if ( 1 === ( $attr['width'] % 2 ) ) {
+ $attr['width'] --;
+ }
+
+ /**
+ * Filter the default VideoPress shortcode options.
+ *
+ * @module videopress
+ *
+ * @since 2.5.0
+ *
+ * @param array $args Array of VideoPress shortcode options.
+ */
+ $options = apply_filters(
+ 'videopress_shortcode_options',
+ array(
+ 'at' => (int) $attr['at'],
+ 'hd' => $attr['hd'],
+ 'loop' => $attr['loop'],
+ 'freedom' => $attr['freedom'],
+ 'autoplay' => $attr['autoplay'],
+ 'permalink' => $attr['permalink'],
+ 'force_flash' => (bool) $attr['flashonly'],
+ 'defaultlangcode' => $attr['defaultlangcode'],
+ 'forcestatic' => false, // This used to be a displayed option, but now is only
+ // accessible via the `videopress_shortcode_options` filter.
+ )
+ );
+
+ // Register VideoPress scripts
+ wp_register_script( 'videopress', 'https://v0.wordpress.com/js/videopress.js', array( 'jquery', 'swfobject' ), '1.09' );
+
+ require_once dirname( __FILE__ ) . '/class.videopress-video.php';
+ require_once dirname( __FILE__ ) . '/class.videopress-player.php';
+
+ $player = new VideoPress_Player( $guid, $attr['width'], $options );
+
+ if ( is_feed() ) {
+ return $player->asXML();
+ } else {
+ return $player->asHTML();
+ }
+ }
+
+ /**
+ * Override the standard video short tag to also process videopress files as well.
+ *
+ * This will, parse the src given, and if it is a videopress file, it will parse as the
+ * VideoPress shortcode instead.
+ *
+ * @param string $html Empty variable to be replaced with shortcode markup.
+ * @param array $attr Attributes of the video shortcode.
+ * @param string $content Video shortcode content.
+ * @param int $instance Unique numeric ID of this video shortcode instance.
+ *
+ * @return string
+ */
+ public function video_shortcode_override( $html, $attr, $content, $instance ) {
+
+ $videopress_guid = null;
+
+ if ( isset( $attr['videopress_guid'] ) ) {
+ $videopress_guid = $attr['videopress_guid'];
+
+ } else {
+ // Handle the different possible url attributes
+ $url_keys = array( 'src', 'mp4' );
+
+ foreach ( $url_keys as $key ) {
+ if ( isset( $attr[ $key ] ) ) {
+ $url = $attr[ $key ];
+ // phpcs:ignore WordPress.WP.CapitalPDangit
+ if ( preg_match( '@videos.(videopress\.com|files\.wordpress\.com)/([a-z0-9]{8})/@i', $url, $matches ) ) {
+ $videopress_guid = $matches[2];
+ }
+
+ // Also test for videopress oembed url, which is used by the Video Media Widget.
+ if ( ! $videopress_guid && preg_match( '@https://videopress.com/v/([a-z0-9]{8})@i', $url, $matches ) ) {
+ $videopress_guid = $matches[1];
+ }
+
+ break;
+ }
+ }
+ }
+
+ if ( $videopress_guid ) {
+ $videopress_attr = array( $videopress_guid );
+ if ( isset( $attr['width'] ) ) {
+ $videopress_attr['w'] = (int) $attr['width'];
+ }
+ if ( isset( $attr['autoplay'] ) ) {
+ $videopress_attr['autoplay'] = $attr['autoplay'];
+ }
+ if ( isset( $attr['loop'] ) ) {
+ $videopress_attr['loop'] = $attr['loop'];
+ }
+
+ // Then display the VideoPress version of the stored GUID!
+ return $this->shortcode_callback( $videopress_attr );
+ }
+
+ return '';
+ }
+
+ /**
+ * Adds a `for` query parameter to the oembed provider request URL.
+ *
+ * @param String $oembed_provider
+ * @return String $ehnanced_oembed_provider
+ */
+ public function add_oembed_for_parameter( $oembed_provider ) {
+ if ( false === stripos( $oembed_provider, 'videopress.com' ) ) {
+ return $oembed_provider;
+ }
+ return add_query_arg( 'for', parse_url( home_url(), PHP_URL_HOST ), $oembed_provider );
+ }
+
+ /**
+ * Register a VideoPress handler for direct links to .mov files (and potential other non-handled types later).
+ */
+ public function add_video_embed_hander() {
+ // These are the video extensions that VideoPress can transcode and considers video as well (even if core does not).
+ $extensions = array( 'mov' );
+ $override_extensions = implode( '|', $extensions );
+
+ $regex = "#^https?://videos.(videopress.com|files.wordpress.com)/.+?.($override_extensions)$#i";
+
+ /** This filter is already documented in core/wp-includes/embed.php */
+ $filter = apply_filters( 'wp_video_embed_handler', 'wp_embed_handler_video' );
+ wp_embed_register_handler( 'video', $regex, $filter, 10 );
+ }
+}
+
+VideoPress_Shortcode::initialize();
diff --git a/plugins/jetpack/modules/videopress/utility-functions.php b/plugins/jetpack/modules/videopress/utility-functions.php
new file mode 100644
index 00000000..345fa719
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/utility-functions.php
@@ -0,0 +1,692 @@
+<?php
+
+/**
+ * We won't have any videos less than sixty pixels wide. That would be silly.
+ */
+defined( 'VIDEOPRESS_MIN_WIDTH' ) or define( 'VIDEOPRESS_MIN_WIDTH', 60 );
+
+/**
+ * Validate user-supplied guid values against expected inputs
+ *
+ * @since 1.1
+ * @param string $guid video identifier
+ * @return bool true if passes validation test
+ */
+function videopress_is_valid_guid( $guid ) {
+ if ( ! empty( $guid ) && is_string( $guid ) && strlen( $guid ) === 8 && ctype_alnum( $guid ) ) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Get details about a specific video by GUID:
+ *
+ * @param $guid string
+ * @return object
+ */
+function videopress_get_video_details( $guid ) {
+ if ( ! videopress_is_valid_guid( $guid ) ) {
+ return new WP_Error( 'bad-guid-format', __( 'Invalid Video GUID!', 'jetpack' ) );
+ }
+
+ $version = '1.1';
+ $endpoint = sprintf( '/videos/%1$s', $guid );
+ $query_url = sprintf(
+ 'https://public-api.wordpress.com/rest/v%1$s%2$s',
+ $version,
+ $endpoint
+ );
+
+ // Look for data in our transient. If nothing, let's make a new query.
+ $data_from_cache = get_transient( 'jetpack_videopress_' . $guid );
+ if ( false === $data_from_cache ) {
+ $response = wp_remote_get( esc_url_raw( $query_url ) );
+ $data = json_decode( wp_remote_retrieve_body( $response ) );
+
+ // Cache the response for an hour.
+ set_transient( 'jetpack_videopress_' . $guid, $data, HOUR_IN_SECONDS );
+ } else {
+ $data = $data_from_cache;
+ }
+
+ /**
+ * Allow functions to modify fetched video details.
+ *
+ * This filter allows third-party code to modify the return data
+ * about a given video. It may involve swapping some data out or
+ * adding new parameters.
+ *
+ * @since 4.0.0
+ *
+ * @param object $data The data returned by the WPCOM API. See: https://developer.wordpress.com/docs/api/1.1/get/videos/%24guid/
+ * @param string $guid The GUID of the VideoPress video in question.
+ */
+ return apply_filters( 'videopress_get_video_details', $data, $guid );
+}
+
+
+/**
+ * Get an attachment ID given a URL.
+ *
+ * Modified from http://wpscholar.com/blog/get-attachment-id-from-wp-image-url/
+ *
+ * @todo: Add some caching in here.
+ *
+ * @param string $url
+ *
+ * @return int|bool Attachment ID on success, false on failure
+ */
+function videopress_get_attachment_id_by_url( $url ) {
+ $wp_upload_dir = wp_upload_dir();
+ // Strip out protocols, so it doesn't fail because searching for http: in https: dir.
+ $dir = set_url_scheme( trailingslashit( $wp_upload_dir['baseurl'] ), 'relative' );
+
+ // Is URL in uploads directory?
+ if ( false !== strpos( $url, $dir ) ) {
+
+ $file = basename( $url );
+
+ $query_args = array(
+ 'post_type' => 'attachment',
+ 'post_status' => 'inherit',
+ 'fields' => 'ids',
+ 'meta_query' => array(
+ array(
+ 'key' => '_wp_attachment_metadata',
+ 'compare' => 'LIKE',
+ 'value' => $file,
+ ),
+ ),
+ );
+
+ $query = new WP_Query( $query_args );
+
+ if ( $query->have_posts() ) {
+ foreach ( $query->posts as $attachment_id ) {
+ $meta = wp_get_attachment_metadata( $attachment_id );
+ $original_file = basename( $meta['file'] );
+ $cropped_files = wp_list_pluck( $meta['sizes'], 'file' );
+
+ if ( $original_file === $file || in_array( $file, $cropped_files ) ) {
+ return (int) $attachment_id;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Similar to `media_sideload_image` -- but returns an ID.
+ *
+ * @param $url
+ * @param $attachment_id
+ *
+ * @return int|mixed|object|WP_Error
+ */
+function videopress_download_poster_image( $url, $attachment_id ) {
+ // Set variables for storage, fix file filename for query strings.
+ preg_match( '/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $url, $matches );
+ if ( ! $matches ) {
+ return new WP_Error( 'image_sideload_failed', __( 'Invalid image URL', 'jetpack' ) );
+ }
+
+ $file_array = array();
+ $file_array['name'] = basename( $matches[0] );
+ $file_array['tmp_name'] = download_url( $url );
+
+ // If error storing temporarily, return the error.
+ if ( is_wp_error( $file_array['tmp_name'] ) ) {
+ return $file_array['tmp_name'];
+ }
+
+ // Do the validation and storage stuff.
+ $thumbnail_id = media_handle_sideload( $file_array, $attachment_id, null );
+
+ // Flag it as poster image, so we can exclude it from display.
+ update_post_meta( $thumbnail_id, 'videopress_poster_image', 1 );
+
+ return $thumbnail_id;
+}
+
+/**
+ * Creates a local media library item of a remote VideoPress video.
+ *
+ * @param $guid
+ * @param int $parent_id
+ *
+ * @return int|object
+ */
+function create_local_media_library_for_videopress_guid( $guid, $parent_id = 0 ) {
+ $vp_data = videopress_get_video_details( $guid );
+ if ( ! $vp_data || is_wp_error( $vp_data ) ) {
+ return $vp_data;
+ }
+
+ $args = array(
+ 'post_date' => $vp_data->upload_date,
+ 'post_title' => wp_kses( $vp_data->title, array() ),
+ 'post_content' => wp_kses( $vp_data->description, array() ),
+ 'post_mime_type' => 'video/videopress',
+ 'guid' => sprintf( 'https://videopress.com/v/%s', $guid ),
+ );
+
+ $attachment_id = wp_insert_attachment( $args, null, $parent_id );
+
+ if ( ! is_wp_error( $attachment_id ) ) {
+ update_post_meta( $attachment_id, 'videopress_guid', $guid );
+ wp_update_attachment_metadata(
+ $attachment_id,
+ array(
+ 'width' => $vp_data->width,
+ 'height' => $vp_data->height,
+ )
+ );
+
+ $thumbnail_id = videopress_download_poster_image( $vp_data->poster, $attachment_id );
+ update_post_meta( $attachment_id, '_thumbnail_id', $thumbnail_id );
+ }
+
+ return $attachment_id;
+}
+
+/**
+ * Helper that will look for VideoPress media items that are more than 30 minutes old,
+ * that have not had anything attached to them by a wpcom upload and deletes the ghost
+ * attachment.
+ *
+ * These happen primarily because of failed upload attempts.
+ *
+ * @return int The number of items that were cleaned up.
+ */
+function videopress_cleanup_media_library() {
+
+ // Disable this job for now.
+ return 0;
+ $query_args = array(
+ 'post_type' => 'attachment',
+ 'post_status' => 'inherit',
+ 'post_mime_type' => 'video/videopress',
+ 'meta_query' => array(
+ array(
+ 'key' => 'videopress_status',
+ 'value' => 'new',
+ ),
+ ),
+ );
+
+ $query = new WP_Query( $query_args );
+
+ $cleaned = 0;
+
+ $now = current_time( 'timestamp' );
+
+ if ( $query->have_posts() ) {
+ foreach ( $query->posts as $post ) {
+ $post_time = strtotime( $post->post_date_gmt );
+
+ // If the post is older than 30 minutes, it is safe to delete it.
+ if ( $now - $post_time > MINUTE_IN_SECONDS * 30 ) {
+ // Force delete the attachment, because we don't want it appearing in the trash.
+ wp_delete_attachment( $post->ID, true );
+
+ $cleaned++;
+ }
+ }
+ }
+
+ return $cleaned;
+}
+
+/**
+ * Return an absolute URI for a given filename and guid on the CDN.
+ * No check is performed to ensure the guid exists or the file is present. Simple centralized string builder.
+ *
+ * @param string $guid VideoPress identifier
+ * @param string $filename name of file associated with the guid (video file name or thumbnail file name)
+ *
+ * @return string Absolute URL of VideoPress file for the given guid.
+ */
+function videopress_cdn_file_url( $guid, $filename ) {
+ return "https://videos.files.wordpress.com/{$guid}/{$filename}";
+}
+
+/**
+ * Get an array of the transcoding status for the given video post.
+ *
+ * @since 4.4
+ * @param int $post_id
+ * @return array|bool Returns an array of statuses if this is a VideoPress post, otherwise it returns false.
+ */
+function videopress_get_transcoding_status( $post_id ) {
+ $meta = wp_get_attachment_metadata( $post_id );
+
+ // If this has not been processed by videopress, we can skip the rest.
+ if ( ! $meta || ! isset( $meta['file_statuses'] ) ) {
+ return false;
+ }
+
+ $info = (object) $meta['file_statuses'];
+
+ $status = array(
+ 'std_mp4' => isset( $info->mp4 ) ? $info->mp4 : null,
+ 'std_ogg' => isset( $info->ogg ) ? $info->ogg : null,
+ 'dvd_mp4' => isset( $info->dvd ) ? $info->dvd : null,
+ 'hd_mp4' => isset( $info->hd ) ? $info->hd : null,
+ );
+
+ return $status;
+}
+
+/**
+ * Get the direct url to the video.
+ *
+ * @since 4.4
+ * @param string $guid
+ * @return string
+ */
+function videopress_build_url( $guid ) {
+
+ // No guid, no videopress url.
+ if ( ! $guid ) {
+ return '';
+ }
+
+ return 'https://videopress.com/v/' . $guid;
+}
+
+/**
+ * Create an empty videopress media item that will be filled out later by an xmlrpc
+ * callback from the VideoPress servers.
+ *
+ * @since 4.4
+ * @param string $title
+ * @return int|WP_Error
+ */
+function videopress_create_new_media_item( $title, $guid = null ) {
+ $post = array(
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'video/videopress',
+ 'post_title' => $title,
+ 'post_content' => '',
+ 'guid' => videopress_build_url( $guid ),
+ );
+
+ $media_id = wp_insert_post( $post );
+
+ add_post_meta( $media_id, 'videopress_status', 'initiated' );
+
+ add_post_meta( $media_id, 'videopress_guid', $guid );
+
+ return $media_id;
+}
+
+
+/**
+ * @param array $current_status
+ * @param array $new_meta
+ * @return array
+ */
+function videopress_merge_file_status( $current_status, $new_meta ) {
+ $new_statuses = array();
+
+ if ( isset( $new_meta['videopress']['files_status']['hd'] ) ) {
+ $new_statuses['hd'] = $new_meta['videopress']['files_status']['hd'];
+ }
+
+ if ( isset( $new_meta['videopress']['files_status']['dvd'] ) ) {
+ $new_statuses['dvd'] = $new_meta['videopress']['files_status']['dvd'];
+ }
+
+ if ( isset( $new_meta['videopress']['files_status']['std']['mp4'] ) ) {
+ $new_statuses['mp4'] = $new_meta['videopress']['files_status']['std']['mp4'];
+ }
+
+ if ( isset( $new_meta['videopress']['files_status']['std']['ogg'] ) ) {
+ $new_statuses['ogg'] = $new_meta['videopress']['files_status']['std']['ogg'];
+ }
+
+ foreach ( $new_statuses as $format => $status ) {
+ if ( ! isset( $current_status[ $format ] ) ) {
+ $current_status[ $format ] = $status;
+ continue;
+ }
+
+ if ( $current_status[ $format ] !== 'DONE' ) {
+ $current_status[ $format ] = $status;
+ }
+ }
+
+ return $current_status;
+}
+
+/**
+ * Check to see if a video has completed processing.
+ *
+ * @since 4.4
+ * @param int $post_id
+ * @return bool
+ */
+function videopress_is_finished_processing( $post_id ) {
+ $post = get_post( $post_id );
+
+ if ( is_wp_error( $post ) ) {
+ return false;
+ }
+
+ $meta = wp_get_attachment_metadata( $post->ID );
+
+ if ( ! isset( $meta['file_statuses'] ) || ! is_array( $meta['file_statuses'] ) ) {
+ return false;
+ }
+
+ $check_statuses = array( 'hd', 'dvd', 'mp4', 'ogg' );
+
+ foreach ( $check_statuses as $status ) {
+ if ( ! isset( $meta['file_statuses'][ $status ] ) || $meta['file_statuses'][ $status ] != 'DONE' ) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+/**
+ * Update the meta information status for the given video post.
+ *
+ * @since 4.4
+ * @param int $post_id
+ * @return bool
+ */
+function videopress_update_meta_data( $post_id ) {
+
+ $meta = wp_get_attachment_metadata( $post_id );
+
+ // If this has not been processed by VideoPress, we can skip the rest.
+ if ( ! $meta || ! isset( $meta['videopress'] ) ) {
+ return false;
+ }
+
+ $info = (object) $meta['videopress'];
+
+ $args = array(
+ // 'sslverify' => false,
+ );
+
+ $result = wp_remote_get( videopress_make_video_get_path( $info->guid ), $args );
+
+ if ( is_wp_error( $result ) ) {
+ return false;
+ }
+
+ $response = json_decode( $result['body'], true );
+
+ // Update the attachment metadata.
+ $meta['videopress'] = $response;
+
+ wp_update_attachment_metadata( $post_id, $meta );
+
+ return true;
+}
+
+/**
+ * Check to see if this is a VideoPress post that hasn't had a guid set yet.
+ *
+ * @param int $post_id
+ * @return bool
+ */
+function videopress_is_attachment_without_guid( $post_id ) {
+ $post = get_post( $post_id );
+
+ if ( is_wp_error( $post ) ) {
+ return false;
+ }
+
+ if ( $post->post_mime_type !== 'video/videopress' ) {
+ return false;
+ }
+
+ $videopress_guid = get_post_meta( $post_id, 'videopress_guid', true );
+
+ if ( $videopress_guid ) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Check to see if this is a VideoPress attachment.
+ *
+ * @param int $post_id
+ * @return bool
+ */
+function is_videopress_attachment( $post_id ) {
+ $post = get_post( $post_id );
+
+ if ( is_wp_error( $post ) ) {
+ return false;
+ }
+
+ if ( $post->post_mime_type !== 'video/videopress' ) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Get the video update path
+ *
+ * @since 4.4
+ * @param string $guid
+ * @return string
+ */
+function videopress_make_video_get_path( $guid ) {
+ return sprintf(
+ '%s://%s/rest/v%s/videos/%s',
+ 'https',
+ JETPACK__WPCOM_JSON_API_HOST,
+ Jetpack_Client::WPCOM_JSON_API_VERSION,
+ $guid
+ );
+}
+
+/**
+ * Get the upload api path.
+ *
+ * @since 4.4
+ * @param int $blog_id The id of the blog we're uploading to.
+ * @return string
+ */
+function videopress_make_media_upload_path( $blog_id ) {
+ return sprintf(
+ 'https://public-api.wordpress.com/rest/v1.1/sites/%s/media/new',
+ $blog_id
+ );
+}
+
+/**
+ * This is a mock of the internal VideoPress method, which is meant to duplicate the functionality
+ * of the WPCOM API, so that the Jetpack REST API returns the same data with no modifications.
+ *
+ * @param int $blog_id Blog ID.
+ * @param int $post_id Post ID.
+ * @return bool|stdClass
+ */
+function video_get_info_by_blogpostid( $blog_id, $post_id ) {
+ $post = get_post( $post_id );
+
+ $video_info = new stdClass();
+ $video_info->post_id = $post_id;
+ $video_info->blog_id = $blog_id;
+ $video_info->guid = null;
+ $video_info->finish_date_gmt = '0000-00-00 00:00:00';
+
+ if ( is_wp_error( $post ) ) {
+ return $video_info;
+ }
+
+ if ( 'video/videopress' !== $post->post_mime_type ) {
+ return $video_info;
+ }
+
+ // Since this is a VideoPress post, lt's fill out the rest of the object.
+ $video_info->guid = get_post_meta( $post_id, 'videopress_guid', true );
+
+ if ( videopress_is_finished_processing( $post_id ) ) {
+ $video_info->finish_date_gmt = date( 'Y-m-d H:i:s' );
+ }
+
+ return $video_info;
+}
+
+
+/**
+ * Check that a VideoPress video format has finished processing.
+ *
+ * This uses the info object, because that is what the WPCOM endpoint
+ * uses, however we don't have a complete info object in the same way
+ * WPCOM does, so we pull the meta information out of the post
+ * options instead.
+ *
+ * Note: This mimics the WPCOM function of the same name and helps the media
+ * API endpoint add all needed VideoPress data.
+ *
+ * @param stdClass $info
+ * @param string $format
+ * @return bool
+ */
+function video_format_done( $info, $format ) {
+
+ // Avoids notice when a non-videopress item is found.
+ if ( ! is_object( $info ) ) {
+ return false;
+ }
+
+ $post_id = $info->post_id;
+
+ if ( get_post_mime_type( $post_id ) !== 'video/videopress' ) {
+ return false;
+ }
+
+ $post = get_post( $post_id );
+
+ if ( is_wp_error( $post ) ) {
+ return false;
+ }
+
+ $meta = wp_get_attachment_metadata( $post->ID );
+
+ switch ( $format ) {
+ case 'fmt_hd':
+ return isset( $meta['videopress']['files']['hd']['mp4'] );
+ break;
+
+ case 'fmt_dvd':
+ return isset( $meta['videopress']['files']['dvd']['mp4'] );
+ break;
+
+ case 'fmt_std':
+ return isset( $meta['videopress']['files']['std']['mp4'] );
+ break;
+
+ case 'fmt_ogg':
+ return isset( $meta['videopress']['files']['std']['ogg'] );
+ break;
+ }
+
+ return false;
+}
+
+/**
+ * Get the image URL for the given VideoPress GUID
+ *
+ * We look up by GUID, because that is what WPCOM does and this needs to be
+ * parameter compatible with that.
+ *
+ * Note: This mimics the WPCOM function of the same name and helps the media
+ * API endpoint add all needed VideoPress data.
+ *
+ * @param string $guid
+ * @param string $format
+ * @return string
+ */
+function video_image_url_by_guid( $guid, $format ) {
+
+ $post = video_get_post_by_guid( $guid );
+
+ if ( is_wp_error( $post ) ) {
+ return null;
+ }
+
+ $meta = wp_get_attachment_metadata( $post->ID );
+
+ // We add ssl => 1 to make sure that the videos.files.wordpress.com domain is parsed as photon.
+ $poster = apply_filters( 'jetpack_photon_url', $meta['videopress']['poster'], array( 'ssl' => 1 ), 'https' );
+
+ return $poster;
+}
+
+/**
+ * Using a GUID, find a post.
+ *
+ * @param string $guid
+ * @return WP_Post
+ */
+function video_get_post_by_guid( $guid ) {
+ $args = array(
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'video/videopress',
+ 'post_status' => 'inherit',
+ 'meta_query' => array(
+ array(
+ 'key' => 'videopress_guid',
+ 'value' => $guid,
+ 'compare' => '=',
+ ),
+ ),
+ );
+
+ $query = new WP_Query( $args );
+
+ $post = $query->next_post();
+
+ return $post;
+}
+
+/**
+ * From the given VideoPress post_id, return back the appropriate attachment URL.
+ *
+ * When the MP4 hasn't been processed yet or this is not a VideoPress video, this will return null.
+ *
+ * @param int $post_id Post ID of the attachment.
+ * @return string|null
+ */
+function videopress_get_attachment_url( $post_id ) {
+
+ // We only handle VideoPress attachments.
+ if ( get_post_mime_type( $post_id ) !== 'video/videopress' ) {
+ return null;
+ }
+
+ $meta = wp_get_attachment_metadata( $post_id );
+
+ if ( ! isset( $meta['videopress']['files']['hd']['mp4'] ) ) {
+ // Use the original file as the url if it isn't transcoded yet.
+ if ( isset( $meta['original'] ) ) {
+ $return = $meta['original'];
+ } else {
+ // Otherwise, there isn't much we can do.
+ return null;
+ }
+ } else {
+ $return = $meta['videopress']['file_url_base']['https'] . $meta['videopress']['files']['hd']['mp4'];
+ }
+
+ // If the URL is a string, return it. Otherwise, we shouldn't to avoid errors downstream, so null.
+ return ( is_string( $return ) ) ? $return : null;
+}
diff --git a/plugins/jetpack/modules/videopress/videopress-admin-rtl.css b/plugins/jetpack/modules/videopress/videopress-admin-rtl.css
new file mode 100644
index 00000000..afb09c61
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/videopress-admin-rtl.css
@@ -0,0 +1,106 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+/**
+ * VideoPress admin media styles
+ */
+.videopress-modal-backdrop {
+ background: #000;
+ opacity: 0.7;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ z-index: 100;
+}
+
+.videopress-modal {
+ padding: 10px 20px;
+ background: white;
+ position: absolute;
+ top: 0;
+ width: 440px;
+ overflow: hidden;
+ right: 50%;
+ margin-right: -220px;
+ z-index: 101;
+ box-shadow: -2px 2px 5px 2px rgba( 0, 0, 0, 0.5 );
+ -webkit-border-bottom-left-radius: 2px;
+ -webkit-border-bottom-right-radius: 2px;
+ border-bottom-left-radius: 2px;
+ border-bottom-right-radius: 2px;
+}
+
+.videopress-modal .submit {
+ text-align: left;
+ padding: 10px 0 5px;
+}
+
+.videopress-preview {
+ display: block;
+ float: left;
+ width: 65%;
+ margin-top: 18px;
+ background: black;
+ min-height: 97px;
+ text-decoration: none;
+}
+
+.vp-preview span.videopress-preview-unavailable {
+ width: 65%;
+ float: left;
+ text-align: right;
+ margin-left: 0;
+}
+
+.videopress-preview img {
+ float: right;
+ width: 100%;
+}
+
+.videopress-preview span {
+ display: block;
+ padding-top: 40px;
+ color: white !important;
+ text-align: center;
+}
+
+.vp-setting .help {
+ margin: 0 35% 4px 0;
+}
+
+.media-sidebar .vp-setting input[type="checkbox"] {
+ float: right;
+ margin-top: 10px;
+}
+
+.vp-setting label {
+ float: right;
+ margin: 8px 5px 0 8px;
+ max-width: 135px;
+}
+
+.vp-setting input[type='radio'] {
+ float: right;
+ margin-top: 9px;
+ width: auto;
+}
+
+.vp-preview span {
+ margin-top: 18px;
+}
+
+.uploader-videopress {
+ margin: 16px;
+}
+
+.uploader-videopress .videopress-errors div {
+ margin: 16px 0;
+}
+
+.compat-field-video-rating input[type="radio"],
+.compat-field-display_embed input[type="checkbox"]{
+ margin-top: -1px !important;
+ margin-left: 5px !important;
+ margin-right: 5px !important;
+ vertical-align: middle;
+}
diff --git a/plugins/jetpack/modules/videopress/videopress-admin-rtl.min.css b/plugins/jetpack/modules/videopress/videopress-admin-rtl.min.css
new file mode 100644
index 00000000..a4675e9e
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/videopress-admin-rtl.min.css
@@ -0,0 +1 @@
+.videopress-modal-backdrop{background:#000;opacity:.7;position:absolute;top:0;width:100%;height:100%;overflow:hidden;z-index:100}.videopress-modal{padding:10px 20px;background:#fff;position:absolute;top:0;width:440px;overflow:hidden;right:50%;margin-right:-220px;z-index:101;box-shadow:-2px 2px 5px 2px rgba(0,0,0,.5);-webkit-border-bottom-left-radius:2px;-webkit-border-bottom-right-radius:2px;border-bottom-left-radius:2px;border-bottom-right-radius:2px}.videopress-modal .submit{text-align:left;padding:10px 0 5px}.videopress-preview{display:block;float:left;width:65%;margin-top:18px;background:#000;min-height:97px;text-decoration:none}.vp-preview span.videopress-preview-unavailable{width:65%;float:left;text-align:right;margin-left:0}.videopress-preview img{float:right;width:100%}.videopress-preview span{display:block;padding-top:40px;color:#fff!important;text-align:center}.vp-setting .help{margin:0 35% 4px 0}.media-sidebar .vp-setting input[type=checkbox]{float:right;margin-top:10px}.vp-setting label{float:right;margin:8px 5px 0 8px;max-width:135px}.vp-setting input[type=radio]{float:right;margin-top:9px;width:auto}.vp-preview span{margin-top:18px}.uploader-videopress{margin:16px}.uploader-videopress .videopress-errors div{margin:16px 0}.compat-field-display_embed input[type=checkbox],.compat-field-video-rating input[type=radio]{margin-top:-1px!important;margin-left:5px!important;margin-right:5px!important;vertical-align:middle} \ No newline at end of file
diff --git a/plugins/jetpack/modules/videopress/videopress-admin.css b/plugins/jetpack/modules/videopress/videopress-admin.css
new file mode 100644
index 00000000..4741b551
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/videopress-admin.css
@@ -0,0 +1,105 @@
+/**
+ * VideoPress admin media styles
+ */
+.videopress-modal-backdrop {
+ background: #000;
+ opacity: 0.7;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ z-index: 100;
+}
+
+.videopress-modal {
+ padding: 10px 20px;
+ background: white;
+ position: absolute;
+ top: 0;
+ width: 440px;
+ overflow: hidden;
+ left: 50%;
+ margin-left: -220px;
+ z-index: 101;
+ box-shadow: 2px 2px 5px 2px rgba( 0, 0, 0, 0.5 );
+ -webkit-border-bottom-right-radius: 2px;
+ -webkit-border-bottom-left-radius: 2px;
+ border-bottom-right-radius: 2px;
+ border-bottom-left-radius: 2px;
+}
+
+.videopress-modal .submit {
+ text-align: right;
+ padding: 10px 0 5px;
+}
+
+.videopress-preview {
+ display: block;
+ float: right;
+ width: 65%;
+ margin-top: 18px;
+ background: black;
+ min-height: 97px;
+ text-decoration: none;
+}
+
+.vp-preview span.videopress-preview-unavailable {
+ width: 65%;
+ float: right;
+ text-align: left;
+ margin-right: 0;
+}
+
+.videopress-preview img {
+ float: left;
+ width: 100%;
+}
+
+.videopress-preview span {
+ display: block;
+ padding-top: 40px;
+ color: white !important;
+ text-align: center;
+}
+
+.vp-setting .help {
+ margin: 0 0 4px 35%;
+}
+
+.media-sidebar .vp-setting input[type="checkbox"] {
+ float: left;
+ margin-top: 10px;
+}
+
+.vp-setting label {
+ float: left;
+ margin: 8px 8px 0 5px;
+ max-width: 135px;
+}
+
+.vp-setting input[type='radio'] {
+ float: left;
+ margin-top: 9px;
+ width: auto;
+}
+
+.vp-preview span {
+ margin-top: 18px;
+}
+
+.uploader-videopress {
+ margin: 16px;
+}
+
+.uploader-videopress .videopress-errors div {
+ margin: 16px 0;
+}
+
+.compat-field-video-rating input[type="radio"],
+.compat-field-display_embed input[type="checkbox"]{
+ margin-top: -1px !important;
+ margin-right: 5px !important;
+ margin-left: 5px !important;
+ vertical-align: middle;
+}
diff --git a/plugins/jetpack/modules/videopress/videopress-admin.min.css b/plugins/jetpack/modules/videopress/videopress-admin.min.css
new file mode 100644
index 00000000..aace7ec9
--- /dev/null
+++ b/plugins/jetpack/modules/videopress/videopress-admin.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.videopress-modal-backdrop{background:#000;opacity:.7;position:absolute;top:0;width:100%;height:100%;overflow:hidden;z-index:100}.videopress-modal{padding:10px 20px;background:#fff;position:absolute;top:0;width:440px;overflow:hidden;left:50%;margin-left:-220px;z-index:101;box-shadow:2px 2px 5px 2px rgba(0,0,0,.5);-webkit-border-bottom-right-radius:2px;-webkit-border-bottom-left-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:2px}.videopress-modal .submit{text-align:right;padding:10px 0 5px}.videopress-preview{display:block;float:right;width:65%;margin-top:18px;background:#000;min-height:97px;text-decoration:none}.vp-preview span.videopress-preview-unavailable{width:65%;float:right;text-align:left;margin-right:0}.videopress-preview img{float:left;width:100%}.videopress-preview span{display:block;padding-top:40px;color:#fff!important;text-align:center}.vp-setting .help{margin:0 0 4px 35%}.media-sidebar .vp-setting input[type=checkbox]{float:left;margin-top:10px}.vp-setting label{float:left;margin:8px 8px 0 5px;max-width:135px}.vp-setting input[type=radio]{float:left;margin-top:9px;width:auto}.vp-preview span{margin-top:18px}.uploader-videopress{margin:16px}.uploader-videopress .videopress-errors div{margin:16px 0}.compat-field-display_embed input[type=checkbox],.compat-field-video-rating input[type=radio]{margin-top:-1px!important;margin-right:5px!important;margin-left:5px!important;vertical-align:middle} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widget-visibility.php b/plugins/jetpack/modules/widget-visibility.php
new file mode 100644
index 00000000..253a9ee1
--- /dev/null
+++ b/plugins/jetpack/modules/widget-visibility.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * Module Name: Widget Visibility
+ * Module Description: Control where widgets appear on your site.
+ * First Introduced: 2.4
+ * Requires Connection: No
+ * Auto Activate: No
+ * Sort Order: 17
+ * Module Tags: Appearance
+ * Feature: Appearance
+ * Additional Search Queries: widget visibility, logic, conditional, widgets, widget
+ */
+
+include dirname( __FILE__ ) . "/widget-visibility/widget-conditions.php";
diff --git a/plugins/jetpack/modules/widget-visibility/widget-conditions.php b/plugins/jetpack/modules/widget-visibility/widget-conditions.php
new file mode 100644
index 00000000..f5e12cbb
--- /dev/null
+++ b/plugins/jetpack/modules/widget-visibility/widget-conditions.php
@@ -0,0 +1,852 @@
+<?php
+
+
+/**
+ * Hide or show widgets conditionally.
+ */
+
+class Jetpack_Widget_Conditions {
+ static $passed_template_redirect = false;
+
+ public static function init() {
+ if ( is_admin() ) {
+ add_action( 'sidebar_admin_setup', array( __CLASS__, 'widget_admin_setup' ) );
+ add_filter( 'widget_update_callback', array( __CLASS__, 'widget_update' ), 10, 3 );
+ add_action( 'in_widget_form', array( __CLASS__, 'widget_conditions_admin' ), 10, 3 );
+ } elseif ( ! in_array( $GLOBALS['pagenow'], array( 'wp-login.php', 'wp-register.php' ) ) ) {
+ add_filter( 'widget_display_callback', array( __CLASS__, 'filter_widget' ) );
+ add_filter( 'sidebars_widgets', array( __CLASS__, 'sidebars_widgets' ) );
+ add_action( 'template_redirect', array( __CLASS__, 'template_redirect' ) );
+ }
+ }
+
+ public static function widget_admin_setup() {
+ wp_enqueue_style( 'widget-conditions', plugins_url( 'widget-conditions/widget-conditions.css', __FILE__ ) );
+ wp_style_add_data( 'widget-conditions', 'rtl', 'replace' );
+ wp_enqueue_script(
+ 'widget-conditions',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/widget-visibility/widget-conditions/widget-conditions.min.js',
+ 'modules/widget-visibility/widget-conditions/widget-conditions.js'
+ ),
+ array( 'jquery', 'jquery-ui-core' ),
+ 20171227,
+ true
+ );
+
+ // Set up a single copy of all of the data that Widget Visibility needs.
+ // This allows all widget conditions to reuse the same data, keeping page size down
+ // and eliminating the AJAX calls we used to have to use to fetch the minor rule options.
+ $widget_conditions_data = array();
+
+ $widget_conditions_data['category'] = array();
+ $widget_conditions_data['category'][] = array( '', __( 'All category pages', 'jetpack' ) );
+
+ $categories = get_categories(
+ array(
+ 'number' => 1000,
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ )
+ );
+ usort( $categories, array( __CLASS__, 'strcasecmp_name' ) );
+
+ foreach ( $categories as $category ) {
+ $widget_conditions_data['category'][] = array( (string) $category->term_id, $category->name );
+ }
+
+ $widget_conditions_data['loggedin'] = array();
+ $widget_conditions_data['loggedin'][] = array( 'loggedin', __( 'Logged In', 'jetpack' ) );
+ $widget_conditions_data['loggedin'][] = array( 'loggedout', __( 'Logged Out', 'jetpack' ) );
+
+ $widget_conditions_data['author'] = array();
+ $widget_conditions_data['author'][] = array( '', __( 'All author pages', 'jetpack' ) );
+
+ // Only users with publish caps
+ $authors = get_users(
+ array(
+ 'orderby' => 'name',
+ 'who' => 'authors',
+ )
+ );
+
+ foreach ( $authors as $author ) {
+ $widget_conditions_data['author'][] = array( (string) $author->ID, $author->display_name );
+ }
+
+ $widget_conditions_data['role'] = array();
+
+ global $wp_roles;
+
+ foreach ( $wp_roles->roles as $role_key => $role ) {
+ $widget_conditions_data['role'][] = array( (string) $role_key, $role['name'] );
+ }
+
+ $widget_conditions_data['tag'] = array();
+ $widget_conditions_data['tag'][] = array( '', __( 'All tag pages', 'jetpack' ) );
+
+ $tags = get_tags(
+ array(
+ 'number' => 1000,
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ )
+ );
+ usort( $tags, array( __CLASS__, 'strcasecmp_name' ) );
+
+ foreach ( $tags as $tag ) {
+ $widget_conditions_data['tag'][] = array( (string) $tag->term_id, $tag->name );
+ }
+
+ $widget_conditions_data['date'] = array();
+ $widget_conditions_data['date'][] = array( '', __( 'All date archives', 'jetpack' ) );
+ $widget_conditions_data['date'][] = array( 'day', __( 'Daily archives', 'jetpack' ) );
+ $widget_conditions_data['date'][] = array( 'month', __( 'Monthly archives', 'jetpack' ) );
+ $widget_conditions_data['date'][] = array( 'year', __( 'Yearly archives', 'jetpack' ) );
+
+ $widget_conditions_data['page'] = array();
+ $widget_conditions_data['page'][] = array( 'front', __( 'Front page', 'jetpack' ) );
+ $widget_conditions_data['page'][] = array( 'posts', __( 'Posts page', 'jetpack' ) );
+ $widget_conditions_data['page'][] = array( 'archive', __( 'Archive page', 'jetpack' ) );
+ $widget_conditions_data['page'][] = array( '404', __( '404 error page', 'jetpack' ) );
+ $widget_conditions_data['page'][] = array( 'search', __( 'Search results', 'jetpack' ) );
+
+ $post_types = get_post_types( array( 'public' => true ), 'objects' );
+
+ $widget_conditions_post_types = array();
+ $widget_conditions_post_type_archives = array();
+
+ foreach ( $post_types as $post_type ) {
+ $widget_conditions_post_types[] = array( 'post_type-' . $post_type->name, $post_type->labels->singular_name );
+ $widget_conditions_post_type_archives[] = array( 'post_type_archive-' . $post_type->name, $post_type->labels->name );
+ }
+
+ $widget_conditions_data['page'][] = array( __( 'Post type:', 'jetpack' ), $widget_conditions_post_types );
+
+ $widget_conditions_data['page'][] = array( __( 'Post type Archives:', 'jetpack' ), $widget_conditions_post_type_archives );
+
+ $pages_dropdown = preg_replace( '/<\/?select[^>]*?>/i', '', wp_dropdown_pages( array( 'echo' => false ) ) );
+
+ preg_match_all( '/value=.([0-9]+).[^>]*>([^<]+)</', $pages_dropdown, $page_ids_and_titles, PREG_SET_ORDER );
+
+ $static_pages = array();
+
+ foreach ( $page_ids_and_titles as $page_id_and_title ) {
+ $static_pages[] = array( (string) $page_id_and_title[1], $page_id_and_title[2] );
+ }
+
+ $widget_conditions_data['page'][] = array( __( 'Static page:', 'jetpack' ), $static_pages );
+
+ $widget_conditions_data['taxonomy'] = array();
+ $widget_conditions_data['taxonomy'][] = array( '', __( 'All taxonomy pages', 'jetpack' ) );
+
+ $taxonomies = get_taxonomies(
+ /**
+ * Filters args passed to get_taxonomies.
+ *
+ * @see https://developer.wordpress.org/reference/functions/get_taxonomies/
+ *
+ * @since 5.3.0
+ *
+ * @module widget-visibility
+ *
+ * @param array $args Widget Visibility taxonomy arguments.
+ */
+ apply_filters( 'jetpack_widget_visibility_tax_args', array( '_builtin' => false ) ),
+ 'objects'
+ );
+
+ usort( $taxonomies, array( __CLASS__, 'strcasecmp_name' ) );
+
+ foreach ( $taxonomies as $taxonomy ) {
+ $taxonomy_terms = get_terms(
+ array( $taxonomy->name ),
+ array(
+ 'number' => 250,
+ 'hide_empty' => false,
+ )
+ );
+
+ $widget_conditions_terms = array();
+ $widget_conditions_terms[] = array( $taxonomy->name, __( 'All pages', 'jetpack' ) );
+
+ foreach ( $taxonomy_terms as $term ) {
+ $widget_conditions_terms[] = array( $taxonomy->name . '_tax_' . $term->term_id, $term->name );
+ }
+
+ $widget_conditions_data['taxonomy'][] = array( $taxonomy->labels->name . ':', $widget_conditions_terms );
+ }
+
+ wp_localize_script( 'widget-conditions', 'widget_conditions_data', $widget_conditions_data );
+
+ // Save a list of the IDs of all pages that have children for dynamically showing the "Include children" checkbox.
+ $all_pages = get_pages();
+ $all_parents = array();
+
+ foreach ( $all_pages as $page ) {
+ if ( $page->post_parent ) {
+ $all_parents[ (string) $page->post_parent ] = true;
+ }
+ }
+
+ $front_page_id = get_option( 'page_on_front' );
+
+ if ( isset( $all_parents[ $front_page_id ] ) ) {
+ $all_parents['front'] = true;
+ }
+
+ wp_localize_script( 'widget-conditions', 'widget_conditions_parent_pages', $all_parents );
+ }
+
+ /**
+ * Add the widget conditions to each widget in the admin.
+ *
+ * @param $widget unused.
+ * @param $return unused.
+ * @param array $instance The widget settings.
+ */
+ public static function widget_conditions_admin( $widget, $return, $instance ) {
+ $conditions = array();
+
+ if ( isset( $instance['conditions'] ) ) {
+ $conditions = $instance['conditions'];
+ }
+
+ if ( ! isset( $conditions['action'] ) ) {
+ $conditions['action'] = 'show';
+ }
+
+ if ( empty( $conditions['rules'] ) ) {
+ $conditions['rules'][] = array(
+ 'major' => '',
+ 'minor' => '',
+ 'has_children' => '',
+ );
+ }
+
+ if ( empty( $conditions['match_all'] ) ) {
+ $conditions['match_all'] = false;
+ }
+
+ ?>
+ <div
+ class="
+ widget-conditional
+ <?php
+ if (
+ empty( $_POST['widget-conditions-visible'] )
+ || $_POST['widget-conditions-visible'] == '0'
+ ) {
+ ?>
+ widget-conditional-hide
+ <?php
+ }
+ ?>
+ <?php
+ if ( ! empty( $conditions['match_all'] ) && $conditions['match_all'] ) {
+ ?>
+ intersection
+ <?php
+ } else {
+ ?>
+ conjunction
+ <?php
+ }
+ ?>
+ ">
+ <input type="hidden" name="widget-conditions-visible" value="
+ <?php
+ if ( isset( $_POST['widget-conditions-visible'] ) ) {
+ echo esc_attr( $_POST['widget-conditions-visible'] ); } else {
+ ?>
+ 0<?php } ?>" />
+ <?php
+ if ( ! isset( $_POST['widget-conditions-visible'] ) ) {
+ ?>
+ <a href="#" class="button display-options"><?php _e( 'Visibility', 'jetpack' ); ?></a><?php } ?>
+ <div class="widget-conditional-inner">
+ <div class="condition-top">
+ <?php printf( _x( '%s if:', 'placeholder: dropdown menu to select widget visibility; hide if or show if', 'jetpack' ), '<select name="conditions[action]"><option value="show" ' . selected( $conditions['action'], 'show', false ) . '>' . esc_html_x( 'Show', 'Used in the "%s if:" translation for the widget visibility dropdown', 'jetpack' ) . '</option><option value="hide" ' . selected( $conditions['action'], 'hide', false ) . '>' . esc_html_x( 'Hide', 'Used in the "%s if:" translation for the widget visibility dropdown', 'jetpack' ) . '</option></select>' ); ?>
+ </div><!-- .condition-top -->
+
+ <div class="conditions">
+ <?php
+
+ foreach ( $conditions['rules'] as $rule_index => $rule ) {
+ $rule = wp_parse_args(
+ $rule,
+ array(
+ 'major' => '',
+ 'minor' => '',
+ 'has_children' => '',
+ )
+ );
+ ?>
+ <div class="condition" data-rule-major="<?php echo esc_attr( $rule['major'] ); ?>" data-rule-minor="<?php echo esc_attr( $rule['minor'] ); ?>" data-rule-has-children="<?php echo esc_attr( $rule['has_children'] ); ?>">
+ <div class="selection alignleft">
+ <select class="conditions-rule-major" name="conditions[rules_major][]">
+ <option value="" <?php selected( '', $rule['major'] ); ?>><?php echo esc_html_x( '-- Select --', 'Used as the default option in a dropdown list', 'jetpack' ); ?></option>
+ <option value="category" <?php selected( 'category', $rule['major'] ); ?>><?php esc_html_e( 'Category', 'jetpack' ); ?></option>
+ <option value="author" <?php selected( 'author', $rule['major'] ); ?>><?php echo esc_html_x( 'Author', 'Noun, as in: "The author of this post is..."', 'jetpack' ); ?></option>
+
+ <?php if ( ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) { // this doesn't work on .com because of caching ?>
+ <option value="loggedin" <?php selected( 'loggedin', $rule['major'] ); ?>><?php echo esc_html_x( 'User', 'Noun', 'jetpack' ); ?></option>
+ <option value="role" <?php selected( 'role', $rule['major'] ); ?>><?php echo esc_html_x( 'Role', 'Noun, as in: "The user role of that can access this widget is..."', 'jetpack' ); ?></option>
+ <?php } ?>
+
+ <option value="tag" <?php selected( 'tag', $rule['major'] ); ?>><?php echo esc_html_x( 'Tag', 'Noun, as in: "This post has one tag."', 'jetpack' ); ?></option>
+ <option value="date" <?php selected( 'date', $rule['major'] ); ?>><?php echo esc_html_x( 'Date', 'Noun, as in: "This page is a date archive."', 'jetpack' ); ?></option>
+ <option value="page" <?php selected( 'page', $rule['major'] ); ?>><?php echo esc_html_x( 'Page', 'Example: The user is looking at a page, not a post.', 'jetpack' ); ?></option>
+ <?php if ( get_taxonomies( array( '_builtin' => false ) ) ) : ?>
+ <option value="taxonomy" <?php selected( 'taxonomy', $rule['major'] ); ?>><?php echo esc_html_x( 'Taxonomy', 'Noun, as in: "This post has one taxonomy."', 'jetpack' ); ?></option>
+ <?php endif; ?>
+ </select>
+
+ <?php _ex( 'is', 'Widget Visibility: {Rule Major [Page]} is {Rule Minor [Search results]}', 'jetpack' ); ?>
+
+ <select class="conditions-rule-minor" name="conditions[rules_minor][]"
+ <?php
+ if ( ! $rule['major'] ) {
+ ?>
+ disabled="disabled"<?php } ?>>
+ <?php
+ /*
+ Include the currently selected value so that if the widget is saved without
+ expanding the Visibility section, we don't lose the minor part of the rule.
+ If it is opened, this list is cleared out and populated with all the values. */
+ ?>
+ <option value="<?php echo esc_attr( $rule['minor'] ); ?>" selected="selected"></option>
+ </select>
+
+ <span class="conditions-rule-has-children"
+ <?php
+ if ( ! $rule['has_children'] ) {
+ ?>
+ style="display: none;"<?php } ?>>
+ <label>
+ <input type="checkbox" name="conditions[page_children][<?php echo $rule_index; ?>]" value="has" <?php checked( $rule['has_children'], true ); ?> />
+ <?php echo esc_html_x( 'Include children', 'Checkbox on Widget Visibility if children of the selected page should be included in the visibility rule.', 'jetpack' ); ?>
+ </label>
+ </span>
+ </div>
+
+ <div class="condition-control">
+ <span class="condition-conjunction">
+ <?php echo esc_html_x( 'or', 'Shown between widget visibility conditions.', 'jetpack' ); ?>
+ </span>
+ <span class="condition-intersection">
+ <?php echo esc_html_x( 'and', 'Shown between widget visibility conditions.', 'jetpack' ); ?>
+ </span>
+ <div class="actions alignright">
+ <a href="#" class="delete-condition dashicons dashicons-no"><?php esc_html_e( 'Delete', 'jetpack' ); ?></a><a href="#" class="add-condition dashicons dashicons-plus"><?php esc_html_e( 'Add', 'jetpack' ); ?></a>
+ </div>
+ </div>
+
+ </div><!-- .condition -->
+ <?php
+ }
+
+ ?>
+ </div><!-- .conditions -->
+ <div class="conditions">
+ <div class="condition-top">
+ <label>
+ <input
+ type="checkbox"
+ name="conditions[match_all]"
+ value="1"
+ class="conditions-match-all"
+ <?php checked( $conditions['match_all'], '1' ); ?> />
+ <?php esc_html_e( 'Match all conditions', 'jetpack' ); ?>
+ </label>
+ </div><!-- .condition-top -->
+ </div><!-- .conditions -->
+ </div><!-- .widget-conditional-inner -->
+ </div><!-- .widget-conditional -->
+ <?php
+ }
+
+ /**
+ * On an AJAX update of the widget settings, process the display conditions.
+ *
+ * @param array $new_instance New settings for this instance as input by the user.
+ * @param array $old_instance Old settings for this instance.
+ * @return array Modified settings.
+ */
+ public static function widget_update( $instance, $new_instance, $old_instance ) {
+ if ( empty( $_POST['conditions'] ) ) {
+ return $instance;
+ }
+
+ $conditions = array();
+ $conditions['action'] = $_POST['conditions']['action'];
+ $conditions['match_all'] = ( isset( $_POST['conditions']['match_all'] ) ? '1' : '0' );
+ $conditions['rules'] = array();
+
+ foreach ( $_POST['conditions']['rules_major'] as $index => $major_rule ) {
+ if ( ! $major_rule ) {
+ continue;
+ }
+
+ $conditions['rules'][] = array(
+ 'major' => $major_rule,
+ 'minor' => isset( $_POST['conditions']['rules_minor'][ $index ] ) ? $_POST['conditions']['rules_minor'][ $index ] : '',
+ 'has_children' => isset( $_POST['conditions']['page_children'][ $index ] ) ? true : false,
+ );
+ }
+
+ if ( ! empty( $conditions['rules'] ) ) {
+ $instance['conditions'] = $conditions;
+ } else {
+ unset( $instance['conditions'] );
+ }
+
+ if (
+ ( isset( $instance['conditions'] ) && ! isset( $old_instance['conditions'] ) )
+ ||
+ (
+ isset( $instance['conditions'], $old_instance['conditions'] )
+ &&
+ serialize( $instance['conditions'] ) != serialize( $old_instance['conditions'] )
+ )
+ ) {
+
+ /**
+ * Fires after the widget visibility conditions are saved.
+ *
+ * @module widget-visibility
+ *
+ * @since 2.4.0
+ */
+ do_action( 'widget_conditions_save' );
+ } elseif ( ! isset( $instance['conditions'] ) && isset( $old_instance['conditions'] ) ) {
+
+ /**
+ * Fires after the widget visibility conditions are deleted.
+ *
+ * @module widget-visibility
+ *
+ * @since 2.4.0
+ */
+ do_action( 'widget_conditions_delete' );
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Filter the list of widgets for a sidebar so that active sidebars work as expected.
+ *
+ * @param array $widget_areas An array of widget areas and their widgets.
+ * @return array The modified $widget_area array.
+ */
+ public static function sidebars_widgets( $widget_areas ) {
+ $settings = array();
+
+ foreach ( $widget_areas as $widget_area => $widgets ) {
+ if ( empty( $widgets ) ) {
+ continue;
+ }
+
+ if ( ! is_array( $widgets ) ) {
+ continue;
+ }
+
+ if ( 'wp_inactive_widgets' == $widget_area ) {
+ continue;
+ }
+
+ foreach ( $widgets as $position => $widget_id ) {
+ // Find the conditions for this widget.
+ if ( preg_match( '/^(.+?)-(\d+)$/', $widget_id, $matches ) ) {
+ $id_base = $matches[1];
+ $widget_number = intval( $matches[2] );
+ } else {
+ $id_base = $widget_id;
+ $widget_number = null;
+ }
+
+ if ( ! isset( $settings[ $id_base ] ) ) {
+ $settings[ $id_base ] = get_option( 'widget_' . $id_base );
+ }
+
+ // New multi widget (WP_Widget)
+ if ( ! is_null( $widget_number ) ) {
+ if ( isset( $settings[ $id_base ][ $widget_number ] ) && false === self::filter_widget( $settings[ $id_base ][ $widget_number ] ) ) {
+ unset( $widget_areas[ $widget_area ][ $position ] );
+ }
+ }
+
+ // Old single widget
+ elseif ( ! empty( $settings[ $id_base ] ) && false === self::filter_widget( $settings[ $id_base ] ) ) {
+ unset( $widget_areas[ $widget_area ][ $position ] );
+ }
+ }
+ }
+
+ return $widget_areas;
+ }
+
+ public static function template_redirect() {
+ self::$passed_template_redirect = true;
+ }
+
+ /**
+ * Generates a condition key based on the rule array
+ *
+ * @param array $rule
+ * @return string key used to retrieve the condition.
+ */
+ static function generate_condition_key( $rule ) {
+ if ( isset( $rule['has_children'] ) ) {
+ return $rule['major'] . ':' . $rule['minor'] . ':' . $rule['has_children'];
+ }
+ return $rule['major'] . ':' . $rule['minor'];
+ }
+
+ /**
+ * Determine whether the widget should be displayed based on conditions set by the user.
+ *
+ * @param array $instance The widget settings.
+ * @return array Settings to display or bool false to hide.
+ */
+ public static function filter_widget( $instance ) {
+ global $wp_query;
+
+ if ( empty( $instance['conditions'] ) || empty( $instance['conditions']['rules'] ) ) {
+ return $instance;
+ }
+
+ // Store the results of all in-page condition lookups so that multiple widgets with
+ // the same visibility conditions don't result in duplicate DB queries.
+ static $condition_result_cache = array();
+
+ $condition_result = false;
+
+ foreach ( $instance['conditions']['rules'] as $rule ) {
+ $condition_result = false;
+ $condition_key = self::generate_condition_key( $rule );
+
+ if ( isset( $condition_result_cache[ $condition_key ] ) ) {
+ $condition_result = $condition_result_cache[ $condition_key ];
+ } else {
+ switch ( $rule['major'] ) {
+ case 'date':
+ switch ( $rule['minor'] ) {
+ case '':
+ $condition_result = is_date();
+ break;
+ case 'month':
+ $condition_result = is_month();
+ break;
+ case 'day':
+ $condition_result = is_day();
+ break;
+ case 'year':
+ $condition_result = is_year();
+ break;
+ }
+ break;
+ case 'page':
+ // Previously hardcoded post type options.
+ if ( 'post' == $rule['minor'] ) {
+ $rule['minor'] = 'post_type-post';
+ } elseif ( ! $rule['minor'] ) {
+ $rule['minor'] = 'post_type-page';
+ }
+
+ switch ( $rule['minor'] ) {
+ case '404':
+ $condition_result = is_404();
+ break;
+ case 'search':
+ $condition_result = is_search();
+ break;
+ case 'archive':
+ $condition_result = is_archive();
+ break;
+ case 'posts':
+ $condition_result = $wp_query->is_posts_page;
+ break;
+ case 'home':
+ $condition_result = is_home();
+ break;
+ case 'front':
+ if ( current_theme_supports( 'infinite-scroll' ) ) {
+ $condition_result = is_front_page();
+ } else {
+ $condition_result = is_front_page() && ! is_paged();
+ }
+ break;
+ default:
+ if ( substr( $rule['minor'], 0, 10 ) == 'post_type-' ) {
+ $condition_result = is_singular( substr( $rule['minor'], 10 ) );
+ } elseif ( substr( $rule['minor'], 0, 18 ) == 'post_type_archive-' ) {
+ $condition_result = is_post_type_archive( substr( $rule['minor'], 18 ) );
+ } elseif ( $rule['minor'] == get_option( 'page_for_posts' ) ) {
+ // If $rule['minor'] is a page ID which is also the posts page
+ $condition_result = $wp_query->is_posts_page;
+ } else {
+ // $rule['minor'] is a page ID
+ $condition_result = is_page() && ( $rule['minor'] == get_the_ID() );
+
+ // Check if $rule['minor'] is parent of page ID
+ if ( ! $condition_result && isset( $rule['has_children'] ) && $rule['has_children'] ) {
+ $condition_result = wp_get_post_parent_id( get_the_ID() ) == $rule['minor'];
+ }
+ }
+ break;
+ }
+ break;
+ case 'tag':
+ // All tag pages.
+ if ( ! $rule['minor'] ) {
+ if ( is_tag() ) {
+ $condition_result = true;
+ } elseif ( is_singular() ) {
+ if ( in_array( 'post_tag', get_post_taxonomies() ) ) {
+ $condition_result = true;
+ }
+ }
+ break;
+ }
+
+ // All pages with the specified tag term.
+ if ( is_tag( $rule['minor'] ) ) {
+ $condition_result = true;
+ } elseif ( is_singular() && has_term( $rule['minor'], 'post_tag' ) ) {
+ $condition_result = true;
+ }
+ break;
+ case 'category':
+ // All category pages.
+ if ( ! $rule['minor'] ) {
+ if ( is_category() ) {
+ $condition_result = true;
+ } elseif ( is_singular() ) {
+ if ( in_array( 'category', get_post_taxonomies() ) ) {
+ $condition_result = true;
+ }
+ }
+ break;
+ }
+
+ // All pages with the specified category term.
+ if ( is_category( $rule['minor'] ) ) {
+ $condition_result = true;
+ } elseif ( is_singular() && has_term( $rule['minor'], 'category' ) ) {
+ $condition_result = true;
+ }
+ break;
+ case 'loggedin':
+ $condition_result = is_user_logged_in();
+ if ( 'loggedin' !== $rule['minor'] ) {
+ $condition_result = ! $condition_result;
+ }
+ break;
+ case 'author':
+ $post = get_post();
+ if ( ! $rule['minor'] && is_author() ) {
+ $condition_result = true;
+ } elseif ( $rule['minor'] && is_author( $rule['minor'] ) ) {
+ $condition_result = true;
+ } elseif ( is_singular() && $rule['minor'] && $rule['minor'] == $post->post_author ) {
+ $condition_result = true;
+ }
+ break;
+ case 'role':
+ if ( is_user_logged_in() ) {
+ $current_user = wp_get_current_user();
+
+ $user_roles = $current_user->roles;
+
+ if ( in_array( $rule['minor'], $user_roles ) ) {
+ $condition_result = true;
+ } else {
+ $condition_result = false;
+ }
+ } else {
+ $condition_result = false;
+ }
+ break;
+ case 'post_type':
+ if ( substr( $rule['minor'], 0, 10 ) == 'post_type-' ) {
+ $condition_result = is_singular( substr( $rule['minor'], 10 ) );
+ } elseif ( substr( $rule['minor'], 0, 18 ) == 'post_type_archive-' ) {
+ $condition_result = is_post_type_archive( substr( $rule['minor'], 18 ) );
+ }
+ break;
+ case 'taxonomy':
+ // All taxonomy pages.
+ if ( ! $rule['minor'] ) {
+ if ( is_archive() ) {
+ if ( is_tag() || is_category() || is_tax() ) {
+ $condition_result = true;
+ }
+ } elseif ( is_singular() ) {
+ $post_taxonomies = get_post_taxonomies();
+ $condition_result = ! empty( $post_taxonomies );
+ }
+ break;
+ }
+
+ // Specified taxonomy page.
+ $term = explode( '_tax_', $rule['minor'] ); // $term[0] = taxonomy name; $term[1] = term id
+ if ( isset( $term[0] ) && isset( $term[1] ) ) {
+ $term[1] = self::maybe_get_split_term( $term[1], $term[0] );
+ }
+
+ // All pages of the specified taxonomy.
+ if ( ! isset( $term[1] ) || ! $term[1] ) {
+ if ( is_tax( $term[0] ) ) {
+ $condition_result = true;
+ } elseif ( is_singular() ) {
+ if ( in_array( $term[0], get_post_taxonomies() ) ) {
+ $condition_result = true;
+ }
+ }
+ break;
+ }
+
+ // All pages with the specified taxonomy term.
+ if ( is_tax( $term[0], $term[1] ) ) {
+ $condition_result = true;
+ } elseif ( is_singular() && has_term( $term[1], $term[0] ) ) {
+ $condition_result = true;
+ }
+ break;
+ }
+
+ if ( $condition_result || self::$passed_template_redirect ) {
+ // Some of the conditions will return false when checked before the template_redirect
+ // action has been called, like is_page(). Only store positive lookup results, which
+ // won't be false positives, before template_redirect, and everything after.
+ $condition_result_cache[ $condition_key ] = $condition_result;
+ }
+ }
+
+ if (
+ isset( $instance['conditions']['match_all'] )
+ && $instance['conditions']['match_all'] == '1'
+ && ! $condition_result
+ ) {
+
+ // In case the match_all flag was set we quit on first failed condition
+ break;
+ } elseif (
+ (
+ empty( $instance['conditions']['match_all'] )
+ || $instance['conditions']['match_all'] !== '1'
+ )
+ && $condition_result
+ ) {
+
+ // Only quit on first condition if the match_all flag was not set
+ break;
+ }
+ }
+
+ if (
+ (
+ 'show' == $instance['conditions']['action']
+ && ! $condition_result
+ ) || (
+ 'hide' == $instance['conditions']['action']
+ && $condition_result
+ )
+ ) {
+ return false;
+ }
+
+ return $instance;
+ }
+
+ public static function strcasecmp_name( $a, $b ) {
+ return strcasecmp( $a->name, $b->name );
+ }
+
+ public static function maybe_get_split_term( $old_term_id = '', $taxonomy = '' ) {
+ $term_id = $old_term_id;
+
+ if ( 'tag' == $taxonomy ) {
+ $taxonomy = 'post_tag';
+ }
+
+ if ( $new_term_id = wp_get_split_term( $old_term_id, $taxonomy ) ) {
+ $term_id = $new_term_id;
+ }
+
+ return $term_id;
+ }
+
+ /**
+ * Upgrade routine to go through all widgets and move the Post Type
+ * setting to its newer location.
+ *
+ * @since 4.7.1
+ */
+ static function migrate_post_type_rules() {
+ global $wp_registered_widgets;
+
+ $sidebars_widgets = get_option( 'sidebars_widgets' );
+
+ // Going through all sidebars and through inactive and orphaned widgets
+ foreach ( $sidebars_widgets as $s => $sidebar ) {
+ if ( ! is_array( $sidebar ) ) {
+ continue;
+ }
+
+ foreach ( $sidebar as $w => $widget ) {
+ // $widget is the id of the widget
+ if ( empty( $wp_registered_widgets[ $widget ] ) ) {
+ continue;
+ }
+
+ $opts = $wp_registered_widgets[ $widget ];
+ $instances = get_option( $opts['callback'][0]->option_name );
+
+ // Going through each instance of the widget
+ foreach ( $instances as $number => $instance ) {
+ if (
+ ! is_array( $instance ) ||
+ empty( $instance['conditions'] ) ||
+ empty( $instance['conditions']['rules'] )
+ ) {
+ continue;
+ }
+
+ // Going through all visibility rules
+ foreach ( $instance['conditions']['rules'] as $index => $rule ) {
+
+ // We only need Post Type rules
+ if ( 'post_type' !== $rule['major'] ) {
+ continue;
+ }
+
+ $rule_type = false;
+
+ // Post type or type archive rule
+ if ( 0 === strpos( $rule['minor'], 'post_type_archive' ) ) {
+ $rule_type = 'post_type_archive';
+ } elseif ( 0 === strpos( $rule['minor'], 'post_type' ) ) {
+ $rule_type = 'post_type';
+ }
+
+ if ( $rule_type ) {
+ $post_type = substr( $rule['minor'], strlen( $rule_type ) + 1 );
+ $rule['minor'] = $rule_type . '-' . $post_type;
+ $rule['major'] = 'page';
+
+ $instances[ $number ]['conditions']['rules'][ $index ] = $rule;
+ }
+ }
+ }
+
+ update_option( $opts['callback'][0]->option_name, $instances );
+ }
+ }
+ }
+
+}
+
+add_action( 'init', array( 'Jetpack_Widget_Conditions', 'init' ) );
diff --git a/plugins/jetpack/modules/widget-visibility/widget-conditions/rtl/widget-conditions-rtl.css b/plugins/jetpack/modules/widget-visibility/widget-conditions/rtl/widget-conditions-rtl.css
new file mode 100644
index 00000000..d2770de6
--- /dev/null
+++ b/plugins/jetpack/modules/widget-visibility/widget-conditions/rtl/widget-conditions-rtl.css
@@ -0,0 +1,115 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.wp-customizer .expanded .widget-conditional .widget-conditional-inner {
+ width: 98%; /* Safari/Chrome, other WebKit */ /* Firefox, other Gecko */
+ box-sizing: border-box;
+}
+
+.wp-customizer .expanded .widget-conditional .form{
+ overflow: scroll;
+ margin-bottom: 20px;
+}
+.widget-liquid-right .widget.expanded {
+ overflow: visible;
+}
+.widget-conditional-hide {
+ display: none;
+}
+.widget-conditional .widget-conditional-inner {
+ background: #F9F9F9;
+ border: 1px solid #DFDFDF;
+ padding: 12px 10px 0;
+
+}
+.widget-conditional {
+ margin-bottom: 12px;
+}
+.widget-conditional .conditions{
+ margin-bottom: 12px;
+}
+.widget-conditional .condition,
+.widget-conditional .condition-top {
+ clear:both;
+}
+.widget-conditional .condition {
+ padding-top: 12px;
+ position: relative;
+}
+.widget-conditional .condition select {
+ width: 120px;
+ position: relative;
+ z-index: 2;
+}
+.widget-conditional .condition-top select {
+ width: auto;
+}
+.widget-conditional .condition-control {
+ padding-top: 4px;
+ clear: both;
+ margin-top: -20px;
+}
+.widget-conditional .selection {
+ margin-left: 50px;
+ margin-right: 20px;
+}
+.widget-conditional .conditions-rule-has-children {
+ display: block;
+}
+.widget-conditional .condition .actions {
+ margin-top: -28px;
+}.widget-conditional .condition .actions {
+ margin-top: -28px;
+}
+
+.widget-conditional .condition-control a {
+ text-decoration: none;
+ position: absolute;
+ top: 17px;
+ text-indent: -9999px;
+ z-index: 1;
+}
+.widget-conditional .condition-control a:before {
+ position: absolute;
+ text-indent: 0;
+ right: 0;
+}
+.widget-conditional .condition-control .delete-condition {
+ right: 0;
+ color: #f11;
+}
+.widget-conditional .condition-control .add-condition {
+ left: 0;
+}
+
+.widget-conditional .condition:last-child .condition-conjunction,
+.widget-conditional .condition:last-child .condition-intersection {
+ display: none;
+}
+
+.widget-conditional.conjunction .condition-intersection {
+ display: none;
+}
+
+.widget-conditional.intersection .condition-conjunction {
+ display: none;
+}
+
+.wp-core-ui .button.display-options {
+ margin-left: 5px;
+}
+.wp-core-ui .button.display-options:hover {
+ text-decoration: none;
+}
+
+.wp-customizer .widget-conditional select {
+ min-width: 0;
+ max-width: none;
+ height: auto;
+}
+.wp-customizer .widget-conditional .condition-control a {
+ top: 15px;
+}
+@media screen and ( max-width: 782px ) {
+ .widget-conditional .condition-control a {
+ top: 20px;
+ }
+}
diff --git a/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions-rtl.css b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions-rtl.css
new file mode 100644
index 00000000..69d6031f
--- /dev/null
+++ b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions-rtl.css
@@ -0,0 +1,116 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.wp-customizer .expanded .widget-conditional .widget-conditional-inner {
+ width: 98%; /* Safari/Chrome, other WebKit */ /* Firefox, other Gecko */
+ box-sizing: border-box;
+}
+
+.wp-customizer .expanded .widget-conditional .form{
+ overflow: scroll;
+ margin-bottom: 20px;
+}
+.widget-liquid-right .widget.expanded {
+ overflow: visible;
+}
+.widget-conditional-hide {
+ display: none;
+}
+.widget-conditional .widget-conditional-inner {
+ background: #F9F9F9;
+ border: 1px solid #DFDFDF;
+ padding: 12px 10px 0;
+
+}
+.widget-conditional {
+ margin-bottom: 12px;
+}
+.widget-conditional .conditions{
+ margin-bottom: 12px;
+}
+.widget-conditional .condition,
+.widget-conditional .condition-top {
+ clear:both;
+}
+.widget-conditional .condition {
+ padding-top: 12px;
+ position: relative;
+}
+.widget-conditional .condition select {
+ width: 120px;
+ position: relative;
+ z-index: 2;
+}
+.widget-conditional .condition-top select {
+ width: auto;
+}
+.widget-conditional .condition-control {
+ padding-top: 4px;
+ clear: both;
+ margin-top: -20px;
+}
+.widget-conditional .selection {
+ margin-left: 50px;
+ margin-right: 20px;
+}
+.widget-conditional .conditions-rule-has-children {
+ display: block;
+}
+.widget-conditional .condition .actions {
+ margin-top: -28px;
+}.widget-conditional .condition .actions {
+ margin-top: -28px;
+}
+
+.widget-conditional .condition-control a {
+ text-decoration: none;
+ position: absolute;
+ top: 17px;
+ text-indent: -9999px;
+ z-index: 1;
+}
+.widget-conditional .condition-control a:before {
+ position: absolute;
+ text-indent: 0;
+ top: 0;
+ right: 0;
+}
+.widget-conditional .condition-control .delete-condition {
+ right: 0;
+ color: #f11;
+}
+.widget-conditional .condition-control .add-condition {
+ left: 0;
+}
+
+.widget-conditional .condition:last-child .condition-conjunction,
+.widget-conditional .condition:last-child .condition-intersection {
+ display: none;
+}
+
+.widget-conditional.conjunction .condition-intersection {
+ display: none;
+}
+
+.widget-conditional.intersection .condition-conjunction {
+ display: none;
+}
+
+.wp-core-ui .button.display-options {
+ margin-left: 5px;
+}
+.wp-core-ui .button.display-options:hover {
+ text-decoration: none;
+}
+
+.wp-customizer .widget-conditional select {
+ min-width: 0;
+ max-width: none;
+ height: auto;
+}
+.wp-customizer .widget-conditional .condition-control a {
+ top: 15px;
+}
+@media screen and ( max-width: 782px ) {
+ .widget-conditional .condition-control a {
+ top: 20px;
+ }
+}
diff --git a/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions-rtl.min.css b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions-rtl.min.css
new file mode 100644
index 00000000..410e7a46
--- /dev/null
+++ b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions-rtl.min.css
@@ -0,0 +1 @@
+.wp-customizer .expanded .widget-conditional .widget-conditional-inner{width:98%;box-sizing:border-box}.wp-customizer .expanded .widget-conditional .form{overflow:scroll;margin-bottom:20px}.widget-liquid-right .widget.expanded{overflow:visible}.widget-conditional-hide{display:none}.widget-conditional .widget-conditional-inner{background:#f9f9f9;border:1px solid #dfdfdf;padding:12px 10px 0}.widget-conditional{margin-bottom:12px}.widget-conditional .conditions{margin-bottom:12px}.widget-conditional .condition,.widget-conditional .condition-top{clear:both}.widget-conditional .condition{padding-top:12px;position:relative}.widget-conditional .condition select{width:120px;position:relative;z-index:2}.widget-conditional .condition-top select{width:auto}.widget-conditional .condition-control{padding-top:4px;clear:both;margin-top:-20px}.widget-conditional .selection{margin-left:50px;margin-right:20px}.widget-conditional .conditions-rule-has-children{display:block}.widget-conditional .condition .actions{margin-top:-28px}.widget-conditional .condition .actions{margin-top:-28px}.widget-conditional .condition-control a{text-decoration:none;position:absolute;top:17px;text-indent:-9999px;z-index:1}.widget-conditional .condition-control a:before{position:absolute;text-indent:0;top:0;right:0}.widget-conditional .condition-control .delete-condition{right:0;color:#f11}.widget-conditional .condition-control .add-condition{left:0}.widget-conditional .condition:last-child .condition-conjunction,.widget-conditional .condition:last-child .condition-intersection{display:none}.widget-conditional.conjunction .condition-intersection{display:none}.widget-conditional.intersection .condition-conjunction{display:none}.wp-core-ui .button.display-options{margin-left:5px}.wp-core-ui .button.display-options:hover{text-decoration:none}.wp-customizer .widget-conditional select{min-width:0;max-width:none;height:auto}.wp-customizer .widget-conditional .condition-control a{top:15px}@media screen and (max-width:782px){.widget-conditional .condition-control a{top:20px}} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.css b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.css
new file mode 100644
index 00000000..72d27b8b
--- /dev/null
+++ b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.css
@@ -0,0 +1,117 @@
+.wp-customizer .expanded .widget-conditional .widget-conditional-inner {
+ width: 98%;
+ -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
+ -moz-box-sizing: border-box; /* Firefox, other Gecko */
+ box-sizing: border-box;
+}
+
+.wp-customizer .expanded .widget-conditional .form{
+ overflow: scroll;
+ margin-bottom: 20px;
+}
+.widget-liquid-right .widget.expanded {
+ overflow: visible;
+}
+.widget-conditional-hide {
+ display: none;
+}
+.widget-conditional .widget-conditional-inner {
+ background: #F9F9F9;
+ border: 1px solid #DFDFDF;
+ padding: 12px 10px 0;
+
+}
+.widget-conditional {
+ margin-bottom: 12px;
+}
+.widget-conditional .conditions{
+ margin-bottom: 12px;
+}
+.widget-conditional .condition,
+.widget-conditional .condition-top {
+ clear:both;
+}
+.widget-conditional .condition {
+ padding-top: 12px;
+ position: relative;
+}
+.widget-conditional .condition select {
+ width: 120px;
+ position: relative;
+ z-index: 2;
+}
+.widget-conditional .condition-top select {
+ width: auto;
+}
+.widget-conditional .condition-control {
+ padding-top: 4px;
+ clear: both;
+ margin-top: -20px;
+}
+.widget-conditional .selection {
+ margin-right: 50px;
+ margin-left: 20px;
+}
+.widget-conditional .conditions-rule-has-children {
+ display: block;
+}
+.widget-conditional .condition .actions {
+ margin-top: -28px;
+}.widget-conditional .condition .actions {
+ margin-top: -28px;
+}
+
+.widget-conditional .condition-control a {
+ text-decoration: none;
+ position: absolute;
+ top: 17px;
+ text-indent: -9999px;
+ z-index: 1;
+}
+.widget-conditional .condition-control a:before {
+ position: absolute;
+ text-indent: 0;
+ top: 0;
+ left: 0;
+}
+.widget-conditional .condition-control .delete-condition {
+ left: 0;
+ color: #f11;
+}
+.widget-conditional .condition-control .add-condition {
+ right: 0;
+}
+
+.widget-conditional .condition:last-child .condition-conjunction,
+.widget-conditional .condition:last-child .condition-intersection {
+ display: none;
+}
+
+.widget-conditional.conjunction .condition-intersection {
+ display: none;
+}
+
+.widget-conditional.intersection .condition-conjunction {
+ display: none;
+}
+
+.wp-core-ui .button.display-options {
+ margin-right: 5px;
+}
+.wp-core-ui .button.display-options:hover {
+ text-decoration: none;
+}
+
+.wp-customizer .widget-conditional select {
+ min-width: 0;
+ max-width: none;
+ height: auto;
+}
+.wp-customizer .widget-conditional .condition-control a {
+ top: 15px;
+}
+@media screen and ( max-width: 782px ) {
+ .widget-conditional .condition-control a {
+ top: 20px;
+ }
+}
diff --git a/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.js b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.js
new file mode 100644
index 00000000..2feca3f4
--- /dev/null
+++ b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.js
@@ -0,0 +1,321 @@
+/* jshint onevar: false, smarttabs: true */
+/* global isRtl */
+/* global widget_conditions_parent_pages */
+/* global widget_conditions_data */
+/* global jQuery */
+
+jQuery( function( $ ) {
+ var widgets_shell = $( 'div#widgets-right' );
+
+ if ( ! widgets_shell.length || ! $( widgets_shell ).find( '.widget-control-actions' ).length ) {
+ widgets_shell = $( 'form#customize-controls' );
+ }
+
+ function setWidgetMargin( $widget ) {
+ var currentWidth, extra;
+
+ if ( $( 'body' ).hasClass( 'wp-customizer' ) ) {
+ // set the inside widget 2 top this way we can see the widget settings
+ $widget.find( '.widget-inside' ).css( 'top', 0 );
+
+ return;
+ }
+
+ if ( $widget.hasClass( 'expanded' ) ) {
+ // The expanded widget must be at least 400px wide in order to
+ // contain the visibility settings. IE wasn't handling the
+ // margin-left value properly.
+
+ if ( $widget.attr( 'style' ) ) {
+ $widget.data( 'original-style', $widget.attr( 'style' ) );
+ }
+
+ currentWidth = $widget.width();
+
+ if ( currentWidth < 400 ) {
+ extra = 400 - currentWidth;
+ if ( isRtl ) {
+ $widget
+ .css( 'position', 'relative' )
+ .css( 'right', '-' + extra + 'px' )
+ .css( 'width', '400px' );
+ } else {
+ $widget
+ .css( 'position', 'relative' )
+ .css( 'left', '-' + extra + 'px' )
+ .css( 'width', '400px' );
+ }
+ }
+ } else if ( $widget.data( 'original-style' ) ) {
+ // Restore any original inline styles when visibility is toggled off.
+ $widget.attr( 'style', $widget.data( 'original-style' ) ).data( 'original-style', null );
+ } else {
+ $widget.removeAttr( 'style' );
+ }
+ }
+
+ function moveWidgetVisibilityButton( $widget ) {
+ var $displayOptionsButton = $widget.find( 'a.display-options' ).first();
+ $displayOptionsButton.insertBefore( $widget.find( 'input.widget-control-save' ) );
+
+ // Widgets with no configurable options don't show the Save button's container.
+ $displayOptionsButton
+ .parent()
+ .removeClass( 'widget-control-noform' )
+ .find( '.spinner' )
+ .remove()
+ .css( 'float', 'left' )
+ .prependTo( $displayOptionsButton.parent() );
+ }
+
+ $( '.widget' ).each( function() {
+ moveWidgetVisibilityButton( $( this ) );
+ } );
+
+ $( document ).on( 'widget-added', function( e, $widget ) {
+ if ( $widget.find( 'div.widget-control-actions a.display-options' ).length === 0 ) {
+ moveWidgetVisibilityButton( $widget );
+ }
+ } );
+
+ widgets_shell.on( 'click.widgetconditions', 'a.add-condition', function( e ) {
+ var $condition = $( this ).closest( 'div.condition' ),
+ $conditionClone = $condition
+ .clone()
+ .data( 'rule-major', '' )
+ .data( 'rule-minor', '' )
+ .data( 'has-children', '' )
+ .insertAfter( $condition );
+
+ e.preventDefault();
+
+ $conditionClone.find( 'select.conditions-rule-major' ).val( '' );
+ $conditionClone
+ .find( 'select.conditions-rule-minor' )
+ .html( '' )
+ .attr( 'disabled' );
+ $conditionClone
+ .find( 'span.conditions-rule-has-children' )
+ .hide()
+ .find( 'input[type="checkbox"]' )
+ .removeAttr( 'checked' );
+
+ resetRuleIndexes( $conditionClone.closest( '.conditions' ) );
+ } );
+
+ widgets_shell.on( 'click.widgetconditions', 'a.display-options', function( e ) {
+ var $displayOptionsButton = $( this ),
+ $widget = $displayOptionsButton.closest( 'div.widget' );
+
+ e.preventDefault();
+
+ $widget.find( 'div.widget-conditional' ).toggleClass( 'widget-conditional-hide' );
+ $( this ).toggleClass( 'active' );
+ $widget.toggleClass( 'expanded' );
+ setWidgetMargin( $widget );
+
+ if ( $( this ).hasClass( 'active' ) ) {
+ $widget.find( 'input[name=widget-conditions-visible]' ).val( '1' );
+ $widget.find( '.condition' ).each( function() {
+ buildMinorConditions( $( this ) );
+ } );
+ } else {
+ $widget.find( 'input[name=widget-conditions-visible]' ).val( '0' );
+ }
+ } );
+
+ widgets_shell.on( 'click.widgetconditions', 'a.delete-condition', function( e ) {
+ var $condition = $( this ).closest( 'div.condition' );
+
+ e.preventDefault();
+
+ if ( $condition.is( ':first-child' ) && $condition.is( ':last-child' ) ) {
+ $( this )
+ .closest( 'div.widget' )
+ .find( 'a.display-options' )
+ .click();
+ $condition
+ .find( 'select.conditions-rule-major' )
+ .val( '' )
+ .change();
+ } else {
+ $condition.find( 'select.conditions-rule-major' ).change();
+ $condition.detach();
+ }
+
+ resetRuleIndexes( $condition.closest( '.conditions' ) );
+ } );
+
+ widgets_shell.on( 'click.widgetconditions', 'div.widget-top', function() {
+ var $widget = $( this ).closest( 'div.widget' ),
+ $displayOptionsButton = $widget.find( 'a.display-options' );
+
+ if ( $displayOptionsButton.hasClass( 'active' ) ) {
+ $displayOptionsButton.attr( 'opened', 'true' );
+ }
+
+ if ( $displayOptionsButton.attr( 'opened' ) ) {
+ $displayOptionsButton.removeAttr( 'opened' );
+ $widget.toggleClass( 'expanded' );
+ setWidgetMargin( $widget );
+ }
+ } );
+
+ widgets_shell.on( 'change.widgetconditions', 'input.conditions-match-all', function() {
+ $( this )
+ .parents( '.widget-conditional' )
+ .toggleClass( 'conjunction' )
+ .toggleClass( 'intersection' );
+ } );
+
+ $( document ).on( 'change.widgetconditions', 'select.conditions-rule-major', function() {
+ var $conditionsRuleMajor = $( this ),
+ $conditionsRuleMinor = $conditionsRuleMajor.siblings( 'select.conditions-rule-minor:first' ),
+ $conditionsRuleHasChildren = $conditionsRuleMajor.siblings(
+ 'span.conditions-rule-has-children'
+ ),
+ $condition = $conditionsRuleMinor.closest( '.condition' );
+
+ $condition.data( 'rule-minor', '' ).data( 'rule-major', $conditionsRuleMajor.val() );
+
+ if ( $conditionsRuleMajor.val() ) {
+ buildMinorConditions( $condition );
+ } else {
+ $conditionsRuleMajor
+ .siblings( 'select.conditions-rule-minor' )
+ .attr( 'disabled', 'disabled' )
+ .html( '' );
+ $conditionsRuleHasChildren
+ .hide()
+ .find( 'input[type="checkbox"]' )
+ .removeAttr( 'checked' );
+ }
+ } );
+
+ $( document ).on( 'change.widgetconditions', 'select.conditions-rule-minor', function() {
+ var $conditionsRuleMinor = $( this ),
+ $conditionsRuleMajor = $conditionsRuleMinor.siblings( 'select.conditions-rule-major' ),
+ $conditionsRuleHasChildren = $conditionsRuleMinor.siblings(
+ 'span.conditions-rule-has-children'
+ ),
+ $condition = $conditionsRuleMinor.closest( '.condition' );
+
+ $condition.data( 'rule-minor', $conditionsRuleMinor.val() );
+
+ if ( $conditionsRuleMajor.val() === 'page' ) {
+ if ( $conditionsRuleMinor.val() in widget_conditions_parent_pages ) {
+ $conditionsRuleHasChildren.show();
+ } else {
+ $conditionsRuleHasChildren
+ .hide()
+ .find( 'input[type="checkbox"]' )
+ .removeAttr( 'checked' );
+ }
+ } else {
+ $conditionsRuleHasChildren
+ .hide()
+ .find( 'input[type="checkbox"]' )
+ .removeAttr( 'checked' );
+ }
+ } );
+
+ $( document ).on( 'widget-updated widget-synced', function( e, widget ) {
+ widget.find( '.condition' ).each( function() {
+ buildMinorConditions( $( this ) );
+ } );
+ } );
+
+ function buildMinorConditions( condition ) {
+ var minor,
+ hasChildren,
+ majorData,
+ i,
+ j,
+ key,
+ val,
+ _len,
+ _jlen,
+ subkey,
+ subval,
+ optgroup,
+ select = condition.find( '.conditions-rule-minor' ).html( '' ),
+ major = condition.data( 'rule-major' );
+
+ // Disable the select, if major rule is empty or if it's a `post_type`.
+ // "Post Type" rule has been removed in Jetpack 4.7, and
+ // because it breaks all other rules we should `return`.
+ if ( ! major || 'post_type' === major ) {
+ select.attr( 'disabled', 'disabled' );
+ return;
+ }
+
+ minor = condition.data( 'rule-minor' );
+ hasChildren = condition.data( 'rule-has-children' );
+ majorData = widget_conditions_data[ major ];
+
+ for ( i = 0, _len = majorData.length; i < _len; i++ ) {
+ key = majorData[ i ][ 0 ];
+ val = majorData[ i ][ 1 ];
+
+ if ( typeof val === 'object' ) {
+ optgroup = $( '<optgroup/>' ).attr( 'label', key );
+
+ for ( j = 0, _jlen = val.length; j < _jlen; j++ ) {
+ subkey = majorData[ i ][ 1 ][ j ][ 0 ];
+ subval = majorData[ i ][ 1 ][ j ][ 1 ];
+
+ optgroup.append(
+ $( '<option/>' )
+ .val( subkey )
+ .text( decodeEntities( subval.replace( /&nbsp;/g, '\xA0' ) ) )
+ );
+ }
+
+ select.append( optgroup );
+ } else {
+ select.append(
+ $( '<option/>' )
+ .val( key )
+ .text( decodeEntities( val.replace( /&nbsp;/g, '\xA0' ) ) )
+ );
+ }
+ }
+
+ select.removeAttr( 'disabled' );
+ select.val( minor );
+
+ if ( 'page' === major && minor in widget_conditions_parent_pages ) {
+ select.siblings( 'span.conditions-rule-has-children' ).show();
+
+ if ( hasChildren ) {
+ select
+ .siblings( 'span.conditions-rule-has-children' )
+ .find( 'input[type="checkbox"]' )
+ .attr( 'checked', 'checked' );
+ }
+ } else {
+ select
+ .siblings( 'span.conditions-rule-has-children' )
+ .hide()
+ .find( 'input[type="checkbox"]' )
+ .removeAttr( 'checked' );
+ }
+ }
+
+ function resetRuleIndexes( widget ) {
+ var index = 0;
+ widget
+ .find( 'span.conditions-rule-has-children' )
+ .find( 'input[type="checkbox"]' )
+ .each( function() {
+ $( this ).attr( 'name', 'conditions[page_children][' + index + ']' );
+ index++;
+ } );
+ }
+
+ function decodeEntities( encodedString ) {
+ var textarea = document.createElement( 'textarea' );
+ textarea.innerHTML = encodedString;
+ return textarea.value;
+ }
+} );
diff --git a/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.min.css b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.min.css
new file mode 100644
index 00000000..33ba43d1
--- /dev/null
+++ b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.wp-customizer .expanded .widget-conditional .widget-conditional-inner{width:98%;box-sizing:border-box}.wp-customizer .expanded .widget-conditional .form{overflow:scroll;margin-bottom:20px}.widget-liquid-right .widget.expanded{overflow:visible}.widget-conditional-hide{display:none}.widget-conditional .widget-conditional-inner{background:#f9f9f9;border:1px solid #dfdfdf;padding:12px 10px 0}.widget-conditional{margin-bottom:12px}.widget-conditional .conditions{margin-bottom:12px}.widget-conditional .condition,.widget-conditional .condition-top{clear:both}.widget-conditional .condition{padding-top:12px;position:relative}.widget-conditional .condition select{width:120px;position:relative;z-index:2}.widget-conditional .condition-top select{width:auto}.widget-conditional .condition-control{padding-top:4px;clear:both;margin-top:-20px}.widget-conditional .selection{margin-right:50px;margin-left:20px}.widget-conditional .conditions-rule-has-children{display:block}.widget-conditional .condition .actions{margin-top:-28px}.widget-conditional .condition .actions{margin-top:-28px}.widget-conditional .condition-control a{text-decoration:none;position:absolute;top:17px;text-indent:-9999px;z-index:1}.widget-conditional .condition-control a:before{position:absolute;text-indent:0;top:0;left:0}.widget-conditional .condition-control .delete-condition{left:0;color:#f11}.widget-conditional .condition-control .add-condition{right:0}.widget-conditional .condition:last-child .condition-conjunction,.widget-conditional .condition:last-child .condition-intersection{display:none}.widget-conditional.conjunction .condition-intersection{display:none}.widget-conditional.intersection .condition-conjunction{display:none}.wp-core-ui .button.display-options{margin-right:5px}.wp-core-ui .button.display-options:hover{text-decoration:none}.wp-customizer .widget-conditional select{min-width:0;max-width:none;height:auto}.wp-customizer .widget-conditional .condition-control a{top:15px}@media screen and (max-width:782px){.widget-conditional .condition-control a{top:20px}} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets.php b/plugins/jetpack/modules/widgets.php
new file mode 100644
index 00000000..84ad7cd0
--- /dev/null
+++ b/plugins/jetpack/modules/widgets.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Module Name: Extra Sidebar Widgets
+ * Module Description: Provides additional widgets for use on your site.
+ * Sort Order: 4
+ * First Introduced: 1.2
+ * Requires Connection: No
+ * Auto Activate: No
+ * Module Tags: Social, Appearance
+ * Feature: Appearance
+ * Additional Search Queries: widget, widgets, facebook, gallery, twitter, gravatar, image, rss
+ */
+
+function jetpack_load_widgets() {
+ $widgets_include = array();
+
+ foreach ( Jetpack::glob_php( dirname( __FILE__ ) . '/widgets' ) as $file ) {
+ $widgets_include[] = $file;
+ }
+ /**
+ * Modify which Jetpack Widgets to register.
+ *
+ * @module widgets
+ *
+ * @since 2.2.1
+ *
+ * @param array $widgets_include An array of widgets to be registered.
+ */
+ $widgets_include = apply_filters( 'jetpack_widgets_to_include', $widgets_include );
+
+ foreach( $widgets_include as $include ) {
+ include_once $include;
+ }
+
+ include_once dirname( __FILE__ ) . '/widgets/migrate-to-core/image-widget.php';
+ include_once dirname( __FILE__ ) . '/widgets/migrate-to-core/gallery-widget.php';
+}
+
+add_action( 'jetpack_modules_loaded', 'jetpack_widgets_loaded' );
+
+function jetpack_widgets_loaded() {
+ Jetpack::enable_module_configurable( __FILE__ );
+ add_filter( 'jetpack_module_configuration_url_widgets', 'jetpack_widgets_configuration_url' );
+}
+
+/**
+ * Overrides default configuration url
+ *
+ * @uses admin_url
+ * @return string module settings URL
+ */
+function jetpack_widgets_configuration_url() {
+ return admin_url( 'customize.php?autofocus[panel]=widgets' );
+}
+
+jetpack_load_widgets();
+
+/**
+ * Enqueue utilities to work with widgets in Customizer.
+ *
+ * @since 4.4.0
+ */
+function jetpack_widgets_customizer_assets_preview() {
+ wp_enqueue_script( 'jetpack-customizer-widget-utils', plugins_url( '/widgets/customizer-utils.js', __FILE__ ), array( 'customize-base' ) );
+}
+add_action( 'customize_preview_init', 'jetpack_widgets_customizer_assets_preview' );
+
+/**
+ * Enqueue styles to stylize widgets in Customizer.
+ *
+ * @since 4.4.0
+ */
+function jetpack_widgets_customizer_assets_controls() {
+ wp_enqueue_style( 'jetpack-customizer-widget-controls', plugins_url( '/widgets/customizer-controls.css', __FILE__ ), array( 'customize-widgets' ) );
+}
+add_action( 'customize_controls_enqueue_scripts', 'jetpack_widgets_customizer_assets_controls' );
+
+function jetpack_widgets_remove_old_widgets() {
+ $old_widgets = array(
+ 'googleplus-badge',
+ );
+
+ // Don't bother cleaning up the sidebars_widgets data.
+ // That will get cleaned up the next time a widget is
+ // added, removed, moved, etc.
+ foreach ( $old_widgets as $old_widget ) {
+ delete_option( "widget_{$old_widget}" );
+ }
+}
+
+add_action( 'updating_jetpack_version', 'jetpack_widgets_remove_old_widgets' );
diff --git a/plugins/jetpack/modules/widgets/authors.php b/plugins/jetpack/modules/widgets/authors.php
new file mode 100644
index 00000000..dfc78652
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/authors.php
@@ -0,0 +1,271 @@
+<?php
+/**
+ * Disable direct access/execution to/of the widget code.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Widget to display blog authors with avatars and recent posts.
+ *
+ * Configurable parameters include:
+ * 1. Whether to display authors who haven't written any posts
+ * 2. The number of posts to be displayed per author (defaults to 0)
+ * 3. Avatar size
+ *
+ * @since 4.5.0
+ */
+class Jetpack_Widget_Authors extends WP_Widget {
+ public function __construct() {
+ parent::__construct(
+ 'authors',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Authors', 'jetpack' ) ),
+ array(
+ 'classname' => 'widget_authors',
+ 'description' => __( 'Display blogs authors with avatars and recent posts.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+
+ if ( is_active_widget( false, false, $this->id_base ) || is_active_widget( false, false, 'monster' ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_style' ) );
+ }
+
+ add_action( 'publish_post', array( __CLASS__, 'flush_cache' ) );
+ add_action( 'deleted_post', array( __CLASS__, 'flush_cache' ) );
+ add_action( 'switch_theme', array( __CLASS__, 'flush_cache' ) );
+ }
+
+ /**
+ * Enqueue stylesheet to adapt the widget to various themes.
+ *
+ * @since 4.5.0
+ */
+ function enqueue_style() {
+ wp_register_style( 'jetpack-authors-widget', plugins_url( 'authors/style.css', __FILE__ ), array(), '20161228' );
+ wp_enqueue_style( 'jetpack-authors-widget' );
+ }
+
+ public static function flush_cache() {
+ wp_cache_delete( 'widget_authors', 'widget' );
+ wp_cache_delete( 'widget_authors_ssl', 'widget' );
+ }
+
+ public function widget( $args, $instance ) {
+ $cache_bucket = is_ssl() ? 'widget_authors_ssl' : 'widget_authors';
+
+ if ( '%BEG_OF_TITLE%' != $args['before_title'] ) {
+ if ( $output = wp_cache_get( $cache_bucket, 'widget' ) ) {
+ echo $output;
+ return;
+ }
+
+ ob_start();
+ }
+
+ $instance = wp_parse_args(
+ $instance, array(
+ 'title' => __( 'Authors', 'jetpack' ),
+ 'all' => false,
+ 'number' => 5,
+ 'avatar_size' => 48,
+ )
+ );
+ $instance['number'] = min( 10, max( 0, (int) $instance['number'] ) );
+
+ // We need to query at least one post to determine whether an author has written any posts or not
+ $query_number = max( $instance['number'], 1 );
+
+ $default_excluded_authors = array();
+ /**
+ * Filter authors from the Widget Authors widget.
+ *
+ * @module widgets
+ *
+ * @since 4.5.0
+ *
+ * @param array $default_excluded_authors Array of user ID's that will be excluded
+ */
+ $excluded_authors = apply_filters( 'jetpack_widget_authors_exclude', $default_excluded_authors );
+
+ $authors = get_users(
+ array(
+ 'fields' => 'all',
+ 'who' => 'authors',
+ 'exclude' => (array) $excluded_authors,
+ )
+ );
+
+ echo $args['before_widget'];
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', $instance['title'] );
+ echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
+ echo '<ul>';
+
+ $default_post_type = 'post';
+ /**
+ * Filter types of posts that will be counted in the widget
+ *
+ * @module widgets
+ *
+ * @since 4.5.0
+ *
+ * @param string|array $default_post_type type(s) of posts to count for the widget.
+ */
+ $post_types = apply_filters( 'jetpack_widget_authors_post_types', $default_post_type );
+
+ foreach ( $authors as $author ) {
+ $r = new WP_Query(
+ array(
+ 'author' => $author->ID,
+ 'posts_per_page' => $query_number,
+ 'post_type' => $post_types,
+ 'post_status' => 'publish',
+ 'no_found_rows' => true,
+ 'has_password' => false,
+ )
+ );
+
+ if ( ! $r->have_posts() && ! $instance['all'] ) {
+ continue;
+ }
+
+ echo '<li>';
+
+ // Display avatar and author name
+ if ( $r->have_posts() ) {
+ echo '<a href="' . get_author_posts_url( $author->ID ) . '">';
+
+ if ( $instance['avatar_size'] > 1 ) {
+ echo ' ' . get_avatar( $author->ID, $instance['avatar_size'], '', true ) . ' ';
+ }
+
+ echo '<strong>' . esc_html( $author->display_name ) . '</strong>';
+ echo '</a>';
+ } elseif ( $instance['all'] ) {
+ if ( $instance['avatar_size'] > 1 ) {
+ echo get_avatar( $author->ID, $instance['avatar_size'], '', true ) . ' ';
+ }
+
+ echo '<strong>' . esc_html( $author->display_name ) . '</strong>';
+ }
+
+ if ( 0 == $instance['number'] ) {
+ echo '</li>';
+ continue;
+ }
+
+ // Display a short list of recent posts for this author
+
+ if ( $r->have_posts() ) {
+ echo '<ul>';
+
+ while ( $r->have_posts() ) {
+ $r->the_post();
+ echo '<li><a href="' . get_permalink() . '">';
+
+ if ( get_the_title() ) {
+ echo get_the_title();
+ } else {
+ echo get_the_ID();
+ }
+
+ echo '</a></li>';
+ }
+
+ echo '</ul>';
+ }
+
+ echo '</li>';
+ }
+
+ echo '</ul>';
+ echo $args['after_widget'];
+
+ wp_reset_postdata();
+
+ if ( '%BEG_OF_TITLE%' != $args['before_title'] ) {
+ wp_cache_add( $cache_bucket, ob_get_flush(), 'widget' );
+ }
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'authors' );
+ }
+
+ public function form( $instance ) {
+ $instance = wp_parse_args(
+ $instance, array(
+ 'title' => '',
+ 'all' => false,
+ 'avatar_size' => 48,
+ 'number' => 5,
+ )
+ );
+
+ ?>
+ <p>
+ <label>
+ <?php _e( 'Title:', 'jetpack' ); ?>
+ <input class="widefat" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" />
+ </label>
+ </p>
+ <p>
+ <label>
+ <input class="checkbox" type="checkbox" <?php checked( $instance['all'] ); ?> name="<?php echo $this->get_field_name( 'all' ); ?>" />
+ <?php _e( 'Display all authors (including those who have not written any posts)', 'jetpack' ); ?>
+ </label>
+ </p>
+ <p>
+ <label>
+ <?php _e( 'Number of posts to show for each author:', 'jetpack' ); ?>
+ <input style="width: 50px; text-align: center;" name="<?php echo $this->get_field_name( 'number' ); ?>" type="text" value="<?php echo esc_attr( $instance['number'] ); ?>" />
+ <?php _e( '(at most 10)', 'jetpack' ); ?>
+ </label>
+ </p>
+ <p>
+ <label>
+ <?php _e( 'Avatar Size (px):', 'jetpack' ); ?>
+ <select name="<?php echo $this->get_field_name( 'avatar_size' ); ?>">
+ <?php
+ foreach ( array(
+ '1' => __( 'No Avatars', 'jetpack' ),
+ '16' => '16x16',
+ '32' => '32x32',
+ '48' => '48x48',
+ '96' => '96x96',
+ '128' => '128x128',
+ ) as $value => $label ) {
+?>
+ <option value="<?php echo esc_attr( $value ); ?>" <?php selected( $value, $instance['avatar_size'] ); ?>><?php echo esc_html( $label ); ?></option>
+ <?php } ?>
+ </select>
+ </label>
+ </p>
+ <?php
+ }
+
+ /**
+ * Updates the widget on save and flushes cache.
+ *
+ * @param array $new_instance
+ * @param array $old_instance
+ * @return array
+ */
+ public function update( $new_instance, $old_instance ) {
+ $new_instance['title'] = strip_tags( $new_instance['title'] );
+ $new_instance['all'] = isset( $new_instance['all'] );
+ $new_instance['number'] = (int) $new_instance['number'];
+ $new_instance['avatar_size'] = (int) $new_instance['avatar_size'];
+
+ Jetpack_Widget_Authors::flush_cache();
+
+ return $new_instance;
+ }
+}
+
+add_action( 'widgets_init', 'jetpack_register_widget_authors' );
+function jetpack_register_widget_authors() {
+ register_widget( 'Jetpack_Widget_Authors' );
+};
diff --git a/plugins/jetpack/modules/widgets/authors/style.css b/plugins/jetpack/modules/widgets/authors/style.css
new file mode 100644
index 00000000..17ca1b69
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/authors/style.css
@@ -0,0 +1,25 @@
+/* Authors Widget */
+.widget_authors > ul, .widget.widget_authors li > ul {
+ margin-left: inherit;
+ padding-left: 0;
+}
+.widget_authors ul li li {
+ padding-left: 0;
+}
+
+.widget_authors > ul > li {
+ margin-bottom: 1em;
+ list-style: none;
+}
+
+.widget_authors > ul > li + li {
+ border-top: 0;
+}
+
+.widget.widget_authors img {
+ margin-right: 5px;
+ margin-bottom: 5px;
+ vertical-align: middle;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets/blog-stats.php b/plugins/jetpack/modules/widgets/blog-stats.php
new file mode 100644
index 00000000..e89db686
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/blog-stats.php
@@ -0,0 +1,173 @@
+<?php
+/**
+ * Blog Stats Widget.
+ *
+ * @since 4.5.0
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Disable direct access/execution to/of the widget code.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Blog Stats Widget.
+ *
+ * Displays all time stats for that site.
+ *
+ * @since 4.5.0
+ */
+class Jetpack_Blog_Stats_Widget extends WP_Widget {
+
+ /**
+ * Constructor
+ */
+ function __construct() {
+ $widget_ops = array(
+ 'classname' => 'blog-stats',
+ 'description' => esc_html__( 'Show a hit counter for your blog.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ );
+ parent::__construct(
+ 'blog-stats',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', esc_html__( 'Blog Stats', 'jetpack' ) ),
+ $widget_ops
+ );
+ $this->alt_option_name = 'widget_statscounter';
+ }
+
+ /**
+ * Return an associative array of default values
+ *
+ * These values are used in new widgets.
+ *
+ * @return array Array of default values for the Widget's options
+ */
+ public function defaults() {
+ return array(
+ 'title' => esc_html__( 'Blog Stats', 'jetpack' ),
+ /* Translators: Number of views, plural */
+ 'hits' => esc_html__( 'hits', 'jetpack' ),
+ );
+ }
+
+ /**
+ * Return All Time Stats for that blog.
+ *
+ * We query the WordPress.com Stats REST API endpoint.
+ *
+ * @uses stats_get_from_restapi(). That function caches data locally for 5 minutes.
+ *
+ * @return string|false $views All Time Stats for that blog.
+ */
+ public function get_stats() {
+ // Get data from the WordPress.com Stats REST API endpoint.
+ $stats = stats_get_from_restapi( array( 'fields' => 'stats' ) );
+
+ if ( isset( $stats->stats->views ) ) {
+ return $stats->stats->views;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Back end widget form.
+ *
+ * @see WP_Widget::form()
+ *
+ * @param array $instance Previously saved values from database.
+ *
+ * @return void
+ */
+ function form( $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults() );
+ ?>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" />
+ </p>
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'hits' ) ); ?>"><?php echo number_format_i18n( '12345' ); ?></label>
+ <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'hits' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'hits' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['hits'] ); ?>" />
+ </p>
+ <p><?php esc_html_e( 'Hit counter is delayed by up to 60 seconds.', 'jetpack' ); ?></p>
+
+ <?php
+ }
+
+ /**
+ * Sanitize widget form values as they are saved.
+ *
+ * @see WP_Widget::update()
+ *
+ * @param array $new_instance Values just sent to be saved.
+ * @param array $old_instance Previously saved values from database.
+ *
+ * @return array Updated safe values to be saved.
+ */
+ function update( $new_instance, $old_instance ) {
+ $instance = array();
+ $instance['title'] = wp_kses( $new_instance['title'], array() );
+ $instance['hits'] = wp_kses( $new_instance['hits'], array() );
+
+ return $instance;
+ }
+
+ /**
+ * Front-end display of widget.
+ *
+ * @see WP_Widget::widget()
+ *
+ * @param array $args Widget arguments.
+ * @param array $instance Saved values from database.
+ */
+ function widget( $args, $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults() );
+
+ /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
+ $title = apply_filters( 'widget_title', $instance['title'] );
+
+ echo $args['before_widget'];
+
+ if ( ! empty( $title ) ) {
+ echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
+ }
+
+ // Get the Site Stats.
+ $views = $this->get_stats();
+
+ if ( ! empty( $views ) ) {
+ printf(
+ '<ul><li>%1$s %2$s</li></ul>',
+ number_format_i18n( $views ),
+ isset( $instance['hits'] ) ? esc_html( $instance['hits'] ) : ''
+ );
+ } else {
+ esc_html_e( 'There was an issue retrieving stats. Please try again later.', 'jetpack' );
+ }
+
+ echo $args['after_widget'];
+
+ /** This action is already documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'blog_stats' );
+ }
+}
+
+/**
+ * If the Stats module is active in a recent version of Jetpack, register the widget.
+ *
+ * @since 4.5.0
+ */
+function jetpack_blog_stats_widget_init() {
+ if ( function_exists( 'stats_get_from_restapi' ) ) {
+ register_widget( 'Jetpack_Blog_Stats_Widget' );
+ }
+}
+add_action( 'widgets_init', 'jetpack_blog_stats_widget_init' );
diff --git a/plugins/jetpack/modules/widgets/contact-info.php b/plugins/jetpack/modules/widgets/contact-info.php
new file mode 100644
index 00000000..93b4695b
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/contact-info.php
@@ -0,0 +1,394 @@
+<?php
+
+if ( ! class_exists( 'Jetpack_Contact_Info_Widget' ) ) {
+
+ //register Contact_Info_Widget widget
+ function jetpack_contact_info_widget_init() {
+ register_widget( 'Jetpack_Contact_Info_Widget' );
+ }
+
+ add_action( 'widgets_init', 'jetpack_contact_info_widget_init' );
+
+ /**
+ * Makes a custom Widget for displaying Restaurant Location/Map, Hours, and Contact Info available.
+ *
+ * @package WordPress
+ */
+ class Jetpack_Contact_Info_Widget extends WP_Widget {
+
+ /**
+ * Constructor
+ */
+ function __construct() {
+ $widget_ops = array(
+ 'classname' => 'widget_contact_info',
+ 'description' => __( 'Display a map with your location, hours, and contact information.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ );
+ parent::__construct(
+ 'widget_contact_info',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Contact Info & Map', 'jetpack' ) ),
+ $widget_ops
+ );
+ $this->alt_option_name = 'widget_contact_info';
+
+ if ( is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+ }
+ }
+
+ /**
+ * Enqueue scripts and styles.
+ */
+ public function enqueue_scripts() {
+ wp_enqueue_style( 'contact-info-map-css', plugins_url( 'contact-info/contact-info-map.css', __FILE__ ), null, 20160623 );
+ }
+
+
+ /**
+ * Return an associative array of default values
+ *
+ * These values are used in new widgets.
+ *
+ * @return array Array of default values for the Widget's options
+ */
+ public function defaults() {
+ return array(
+ 'title' => __( 'Hours & Info', 'jetpack' ),
+ 'address' => __( "3999 Mission Boulevard,\nSan Diego CA 92109", 'jetpack' ),
+ 'phone' => _x( '1-202-555-1212', 'Example of a phone number', 'jetpack' ),
+ 'hours' => __( "Lunch: 11am - 2pm \nDinner: M-Th 5pm - 11pm, Fri-Sat:5pm - 1am", 'jetpack' ),
+ 'email' => null,
+ 'showmap' => 0,
+ 'apikey' => null,
+ 'lat' => null,
+ 'lon' => null,
+ );
+ }
+
+ /**
+ * Outputs the HTML for this widget.
+ *
+ * @param array $args An array of standard parameters for widgets in this theme
+ * @param array $instance An array of settings for this widget instance
+ *
+ * @return void Echoes it's output
+ **/
+ function widget( $args, $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults() );
+
+ echo $args['before_widget'];
+
+ if ( '' != $instance['title'] ) {
+ echo $args['before_title'] . $instance['title'] . $args['after_title'];
+ }
+
+ /**
+ * Fires at the beginning of the Contact Info widget, after the title.
+ *
+ * @module widgets
+ *
+ * @since 3.9.2
+ */
+ do_action( 'jetpack_contact_info_widget_start' );
+
+ echo '<div itemscope itemtype="http://schema.org/LocalBusiness">';
+
+ if ( '' != $instance['address'] ) {
+
+ $showmap = $instance['showmap'];
+
+ /** This action is documented in modules/widgets/contact-info.php */
+ if ( $showmap && $this->has_good_map( $instance ) ) {
+ /**
+ * Set a Google Maps API Key.
+ *
+ * @since 4.1.0
+ *
+ * @param string $api_key Google Maps API Key
+ */
+ $api_key = apply_filters( 'jetpack_google_maps_api_key', $instance['apikey'] );
+ echo $this->build_map( $instance['address'], $api_key );
+ }
+
+ $map_link = $this->build_map_link( $instance['address'] );
+
+ echo '<div class="confit-address" itemscope itemtype="http://schema.org/PostalAddress" itemprop="address"><a href="' . esc_url( $map_link ) . '" target="_blank">' . str_replace( "\n", '<br/>', esc_html( $instance['address'] ) ) . '</a></div>';
+ }
+
+ if ( '' != $instance['phone'] ) {
+ if ( wp_is_mobile() ) {
+ echo '<div class="confit-phone"><span itemprop="telephone"><a href="' . esc_url( 'tel:' . $instance['phone'] ) . '">' . esc_html( $instance['phone'] ) . '</a></span></div>';
+ } else {
+ echo '<div class="confit-phone"><span itemprop="telephone">' . esc_html( $instance['phone'] ) . '</span></div>';
+ }
+ }
+
+ if ( is_email( trim( $instance['email'] ) ) ) {
+ printf(
+ '<div class="confit-email"><a href="mailto:%1$s">%1$s</a></div>',
+ esc_html( $instance['email'] )
+ );
+ }
+
+ if ( '' != $instance['hours'] ) {
+ echo '<div class="confit-hours" itemprop="openingHours">' . str_replace( "\n", '<br/>', esc_html( $instance['hours'] ) ) . '</div>';
+ }
+
+ echo '</div>';
+
+ /**
+ * Fires at the end of Contact Info widget.
+ *
+ * @module widgets
+ *
+ * @since 3.9.2
+ */
+ do_action( 'jetpack_contact_info_widget_end' );
+
+ echo $args['after_widget'];
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'contact_info' );
+ }
+
+
+ /**
+ * Deals with the settings when they are saved by the admin. Here is
+ * where any validation should be dealt with.
+ *
+ * @param array $new_instance New configuration values
+ * @param array $old_instance Old configuration values
+ *
+ * @return array
+ */
+ function update( $new_instance, $old_instance ) {
+ $update_lat_lon = false;
+ if (
+ ! isset( $old_instance['address'] ) ||
+ $this->urlencode_address( $old_instance['address'] ) != $this->urlencode_address( $new_instance['address'] )
+ ) {
+ $update_lat_lon = true;
+ }
+
+ $instance = array();
+ $instance['title'] = wp_kses( $new_instance['title'], array() );
+ $instance['address'] = wp_kses( $new_instance['address'], array() );
+ $instance['phone'] = wp_kses( $new_instance['phone'], array() );
+ $instance['email'] = wp_kses( $new_instance['email'], array() );
+ $instance['hours'] = wp_kses( $new_instance['hours'], array() );
+ $instance['apikey'] = wp_kses( isset( $new_instance['apikey'] ) ? $new_instance['apikey'] : $old_instance['apikey'], array() );
+ $instance['lat'] = isset( $old_instance['lat'] ) ? floatval( $old_instance['lat'] ) : 0;
+ $instance['lon'] = isset( $old_instance['lon'] ) ? floatval( $old_instance['lon'] ) : 0;
+
+ if ( ! $instance['lat'] || ! $instance['lon'] ) {
+ $update_lat_lon = true;
+ }
+
+ if ( $instance['address'] && $update_lat_lon ) {
+
+ // Get the lat/lon of the user specified address.
+ $address = $this->urlencode_address( $instance['address'] );
+ $path = 'https://maps.googleapis.com/maps/api/geocode/json?sensor=false&address=' . $address;
+ /** This action is documented in modules/widgets/contact-info.php */
+ $key = apply_filters( 'jetpack_google_maps_api_key', $instance['apikey'] );
+
+ if ( ! empty( $key ) ) {
+ $path = add_query_arg( 'key', $key, $path );
+ }
+ $json = wp_remote_retrieve_body( wp_remote_get( esc_url( $path, null, null ) ) );
+
+ if ( ! $json ) {
+ // The read failed :(
+ esc_html_e( 'There was a problem getting the data to display this address on a map. Please refresh your browser and try again.', 'jetpack' );
+ die();
+ }
+
+ $json_obj = json_decode( $json );
+
+ if ( 'ZERO_RESULTS' == $json_obj->status ) {
+ // The address supplied does not have a matching lat / lon.
+ // No map is available.
+ $instance['lat'] = '0';
+ $instance['lon'] = '0';
+ } else {
+
+ $loc = $json_obj->results[0]->geometry->location;
+
+ $lat = floatval( $loc->lat );
+ $lon = floatval( $loc->lng );
+
+ $instance['lat'] = "$lat";
+ $instance['lon'] = "$lon";
+ }
+ }
+
+ if ( ! isset( $new_instance['showmap'] ) ) {
+ $instance['showmap'] = 0;
+ } else {
+ $instance['showmap'] = intval( $new_instance['showmap'] );
+ }
+
+ return $instance;
+ }
+
+
+ /**
+ * Displays the form for this widget on the Widgets page of the WP Admin area.
+ *
+ * @param array $instance Instance configuration.
+ *
+ * @return void
+ */
+ function form( $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults() );
+ wp_enqueue_script(
+ 'contact-info-admin',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/widgets/contact-info/contact-info-admin.min.js',
+ 'modules/widgets/contact-info/contact-info-admin.js'
+ ),
+ array( 'jquery' ),
+ 20160727
+ );
+
+ ?>
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" />
+ </p>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'address' ) ); ?>"><?php esc_html_e( 'Address:', 'jetpack' ); ?></label>
+ <textarea class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'address' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'address' ) ); ?>"><?php echo esc_textarea( $instance['address'] ); ?></textarea>
+ <?php
+ if ( $this->has_good_map( $instance ) ) {
+ ?>
+ <input class="jp-contact-info-showmap" id="<?php echo esc_attr( $this->get_field_id( 'showmap' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'showmap' ) ); ?>" value="1" type="checkbox" <?php checked( $instance['showmap'], 1 ); ?> />
+ <label for="<?php echo esc_attr( $this->get_field_id( 'showmap' ) ); ?>"><?php esc_html_e( 'Show map', 'jetpack' ); ?></label>
+ <?php
+ } else {
+ ?>
+ <span class="error-message"><?php _e( 'Sorry. We can not plot this address. A map will not be displayed. Is the address formatted correctly?', 'jetpack' ); ?></span>
+ <input id="<?php echo esc_attr( $this->get_field_id( 'showmap' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'showmap' ) ); ?>" value="<?php echo( intval( $instance['showmap'] ) ); ?>" type="hidden" />
+ <?php
+ }
+ ?>
+ </p>
+
+ <p class="jp-contact-info-apikey" style="<?php echo $instance['showmap'] ? '' : 'display: none;'; ?>">
+ <label for="<?php echo esc_attr( $this->get_field_id( 'apikey' ) ); ?>">
+ <?php _e( 'Google Maps API Key', 'jetpack' ); ?>
+ <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'apikey' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'apikey' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['apikey'] ); ?>" />
+ <br />
+ <small><?php printf( wp_kses( __( 'Google now requires an API key to use their maps on your site. <a href="%s">See our documentation</a> for instructions on acquiring a key.', 'jetpack' ), array( 'a' => array( 'href' => true ) ) ), 'https://jetpack.com/support/extra-sidebar-widgets/contact-info-widget/' ); ?></small>
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'phone' ) ); ?>"><?php esc_html_e( 'Phone:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'phone' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'phone' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['phone'] ); ?>" />
+ </p>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'email' ) ); ?>"><?php esc_html_e( 'Email Address:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'email' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'email' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['email'] ); ?>" />
+ </p>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'hours' ) ); ?>"><?php esc_html_e( 'Hours:', 'jetpack' ); ?></label>
+ <textarea class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'hours' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'hours' ) ); ?>"><?php echo esc_textarea( $instance['hours'] ); ?></textarea>
+ </p>
+
+ <?php
+ }
+
+
+ /**
+ * Generate a Google Maps link for the supplied address.
+ *
+ * @param string $address Address to link to.
+ *
+ * @return string
+ */
+ function build_map_link( $address ) {
+ // Google map urls have lots of available params but zoom (z) and query (q) are enough.
+ return 'https://maps.google.com/maps?z=16&q=' . $this->urlencode_address( $address );
+ }
+
+
+ /**
+ * Builds map display HTML code from the supplied latitude and longitude.
+ *
+ * @param string $address Address.
+ * @param string $api_key API Key.
+ *
+ * @return string HTML of the map.
+ */
+ function build_map( $address, $api_key = null ) {
+ $this->enqueue_scripts();
+ $src = add_query_arg( 'q', rawurlencode( $address ), 'https://www.google.com/maps/embed/v1/place' );
+ if ( ! empty( $api_key ) ) {
+ $src = add_query_arg( 'key', $api_key, $src );
+ }
+
+ $height = 216;
+
+ $iframe_attributes = sprintf(
+ ' height="%d" frameborder="0" src="%s" class="contact-map"',
+ esc_attr( $height ),
+ esc_url( $src )
+ );
+
+ $iframe_html = sprintf( '<iframe width="600" %s></iframe>', $iframe_attributes );
+
+ if (
+ ! class_exists( 'Jetpack_AMP_Support' )
+ || ! Jetpack_AMP_Support::is_amp_request()
+ ) {
+ return $iframe_html;
+ }
+
+ $amp_iframe_html = sprintf( '<amp-iframe layout="fixed-height" width="auto" sandbox="allow-scripts allow-same-origin" %s>', $iframe_attributes );
+
+ // Add placeholder to avoid AMP error: <amp-iframe> elements must be positioned outside the first 75% of the viewport or 600px from the top (whichever is smaller).
+ $amp_iframe_html .= sprintf( '<span placeholder>%s</span>', esc_html__( 'Loading map&hellip;', 'jetpack' ) );
+
+ // Add original iframe as fallback in case JavaScript is disabled.
+ $amp_iframe_html .= sprintf( '<noscript>%s</noscript>', $iframe_html );
+
+ $amp_iframe_html .= '</amp-iframe>';
+ return $amp_iframe_html;
+ }
+
+ /**
+ * Encode an URL
+ *
+ * @param string $address The URL to encode
+ *
+ * @return string The encoded URL
+ */
+ function urlencode_address( $address ) {
+
+ $address = strtolower( $address );
+ $address = preg_replace( '/\s+/', ' ', trim( $address ) ); // Get rid of any unwanted whitespace
+ $address = str_ireplace( ' ', '+', $address ); // Use + not %20
+ return urlencode( $address );
+ }
+
+ /**
+ * Check if the instance has a valid Map location.
+ *
+ * @param array $instance Widget instance configuration.
+ *
+ * @return bool Whether or not there is a valid map.
+ */
+ function has_good_map( $instance ) {
+ // The lat and lon of an address that could not be plotted will have values of 0 and 0.
+ return ! ( '0' == $instance['lat'] && '0' == $instance['lon'] );
+ }
+
+ }
+
+}
diff --git a/plugins/jetpack/modules/widgets/contact-info/contact-info-admin.js b/plugins/jetpack/modules/widgets/contact-info/contact-info-admin.js
new file mode 100644
index 00000000..f51dccda
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/contact-info/contact-info-admin.js
@@ -0,0 +1,11 @@
+( function( $ ) {
+ $( document ).on( 'change', '.jp-contact-info-showmap', function() {
+ var $checkbox = $( this ),
+ isChecked = $checkbox.is( ':checked' );
+
+ $checkbox
+ .closest( '.widget' )
+ .find( '.jp-contact-info-apikey' )
+ .toggle( isChecked );
+ } );
+} )( window.jQuery );
diff --git a/plugins/jetpack/modules/widgets/contact-info/contact-info-map.css b/plugins/jetpack/modules/widgets/contact-info/contact-info-map.css
new file mode 100644
index 00000000..7aa9e698
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/contact-info/contact-info-map.css
@@ -0,0 +1,4 @@
+.contact-map {
+ max-width: 100%;
+ border: 0;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets/customizer-controls.css b/plugins/jetpack/modules/widgets/customizer-controls.css
new file mode 100644
index 00000000..847bc2f3
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/customizer-controls.css
@@ -0,0 +1,6 @@
+/**
+ * Utilities to stylize widget in Customizer controls.
+ */
+
+/* My Community */
+#available-widgets [class*="community"] .widget-title:before { content: "\f307"; } \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets/customizer-utils.js b/plugins/jetpack/modules/widgets/customizer-utils.js
new file mode 100644
index 00000000..754b6339
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/customizer-utils.js
@@ -0,0 +1,119 @@
+/* global wp, gapi, FB, twttr, PaypalExpressCheckout */
+
+/**
+ * Utilities to work with widgets in Customizer.
+ */
+
+/**
+ * Checks whether this Customizer supports partial widget refresh.
+ * @returns {boolean}
+ */
+wp.customizerHasPartialWidgetRefresh = function() {
+ return (
+ 'object' === typeof wp &&
+ 'function' === typeof wp.customize &&
+ 'object' === typeof wp.customize.selectiveRefresh &&
+ 'object' === typeof wp.customize.widgetsPreview &&
+ 'function' === typeof wp.customize.widgetsPreview.WidgetPartial
+ );
+};
+
+/**
+ * Verifies that the placed widget ID contains the widget name.
+ * @param {object} placement
+ * @param {string} widgetName
+ * @returns {*|boolean}
+ */
+wp.isJetpackWidgetPlaced = function( placement, widgetName ) {
+ return placement.partial.widgetId && 0 === placement.partial.widgetId.indexOf( widgetName );
+};
+
+/**
+ * Bind events for selective refresh in Customizer.
+ */
+( function( $ ) {
+ $( document ).ready( function() {
+ if ( wp && wp.customize && wp.customizerHasPartialWidgetRefresh() ) {
+ // Refresh widget contents when a partial is rendered.
+ wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
+ if ( placement.container ) {
+ // Refresh Google+
+ if (
+ wp.isJetpackWidgetPlaced( placement, 'googleplus-badge' ) &&
+ 'object' === typeof gapi &&
+ gapi.person &&
+ 'function' === typeof gapi.person.go
+ ) {
+ gapi.person.go( placement.container[ 0 ] );
+ }
+
+ // Refresh Facebook XFBML
+ else if (
+ wp.isJetpackWidgetPlaced( placement, 'facebook-likebox' ) &&
+ 'object' === typeof FB &&
+ 'object' === typeof FB.XFBML &&
+ 'function' === typeof FB.XFBML.parse
+ ) {
+ FB.XFBML.parse( placement.container[ 0 ], function() {
+ var $fbContainer = $( placement.container[ 0 ] ).find( '.fb_iframe_widget' ),
+ fbWidth = $fbContainer.data( 'width' ),
+ fbHeight = $fbContainer.data( 'height' );
+ $fbContainer.find( 'span' ).css( { width: fbWidth, height: fbHeight } );
+ setTimeout( function() {
+ $fbContainer
+ .find( 'iframe' )
+ .css( { width: fbWidth, height: fbHeight, position: 'relative' } );
+ }, 1 );
+ } );
+ }
+
+ // Refresh Twitter
+ else if (
+ wp.isJetpackWidgetPlaced( placement, 'twitter_timeline' ) &&
+ 'object' === typeof twttr &&
+ 'object' === typeof twttr.widgets &&
+ 'function' === typeof twttr.widgets.load
+ ) {
+ twttr.widgets.load( placement.container[ 0 ] );
+ } else if ( wp.isJetpackWidgetPlaced( placement, 'eu_cookie_law_widget' ) ) {
+ // Refresh EU Cookie Law
+ if ( $( '#eu-cookie-law' ).hasClass( 'top' ) ) {
+ $( '.widget_eu_cookie_law_widget' ).addClass( 'top' );
+ } else {
+ $( '.widget_eu_cookie_law_widget' ).removeClass( 'top' );
+ }
+ placement.container.fadeIn();
+ } else if ( wp.isJetpackWidgetPlaced( placement, 'jetpack_simple_payments_widget' ) ) {
+ // Refresh Simple Payments Widget
+ try {
+ var buttonId = $( '.jetpack-simple-payments-button', placement.container )
+ .attr( 'id' )
+ .replace( '_button', '' );
+ PaypalExpressCheckout.renderButton( null, null, buttonId, null );
+ } catch ( e ) {
+ // PaypalExpressCheckout may fail.
+ // For the same usage, see also:
+ // https://github.com/Automattic/jetpack/blob/6c1971e6bed7d3df793392a7a58ffe0afaeeb5fe/modules/simple-payments/simple-payments.php#L111
+ }
+ }
+ }
+ } );
+
+ // Refresh widgets when they're moved.
+ wp.customize.selectiveRefresh.bind( 'partial-content-moved', function( placement ) {
+ if ( placement.container ) {
+ // Refresh Twitter timeline iframe, since it has to be re-built.
+ if (
+ wp.isJetpackWidgetPlaced( placement, 'twitter_timeline' ) &&
+ placement.container.find( 'iframe.twitter-timeline:not([src]):first' ).length
+ ) {
+ placement.partial.refresh();
+ } else if ( wp.isJetpackWidgetPlaced( placement, 'jetpack_simple_payments_widget' ) ) {
+ // Refresh Simple Payments Widget
+ placement.partial.refresh();
+ }
+ }
+ } );
+ }
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/widgets/eu-cookie-law.php b/plugins/jetpack/modules/widgets/eu-cookie-law.php
new file mode 100644
index 00000000..62acda4a
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/eu-cookie-law.php
@@ -0,0 +1,301 @@
+<?php
+
+/**
+ * Disable direct access/execution to/of the widget code.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+if ( ! class_exists( 'Jetpack_EU_Cookie_Law_Widget' ) ) {
+ /**
+ * EU Cookie Law Widget
+ *
+ * Display the EU Cookie Law banner in the bottom part of the screen.
+ */
+ class Jetpack_EU_Cookie_Law_Widget extends WP_Widget {
+ /**
+ * EU Cookie Law cookie name.
+ *
+ * @var string
+ */
+ public static $cookie_name = 'eucookielaw';
+
+ /**
+ * Default hide options.
+ *
+ * @var array
+ */
+ private $hide_options = array(
+ 'button',
+ 'scroll',
+ 'time',
+ );
+
+ /**
+ * Default text options.
+ *
+ * @var array
+ */
+ private $text_options = array(
+ 'default',
+ 'custom',
+ );
+
+ /**
+ * Default color scheme options.
+ *
+ * @var array
+ */
+ private $color_scheme_options = array(
+ 'default',
+ 'negative',
+ );
+
+ /**
+ * Default policy URL options.
+ *
+ * @var array
+ */
+ private $policy_url_options = array(
+ 'default',
+ 'custom',
+ );
+
+ /**
+ * Widget position options.
+ *
+ * @var array
+ */
+ private $position_options = array(
+ 'bottom',
+ 'top',
+ );
+
+ /**
+ * Constructor.
+ */
+ function __construct() {
+ parent::__construct(
+ 'eu_cookie_law_widget',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', esc_html__( 'Cookies & Consents Banner', 'jetpack' ) ),
+ array(
+ 'description' => esc_html__( 'Display a banner for EU Cookie Law and GDPR compliance.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ ),
+ array()
+ );
+
+ if ( is_active_widget( false, false, $this->id_base ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) );
+ }
+ }
+
+ /**
+ * Enqueue scripts and styles.
+ */
+ function enqueue_frontend_scripts() {
+ wp_enqueue_style( 'eu-cookie-law-style', plugins_url( 'eu-cookie-law/style.css', __FILE__ ), array(), '20170403' );
+ wp_enqueue_script(
+ 'eu-cookie-law-script',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/widgets/eu-cookie-law/eu-cookie-law.min.js',
+ 'modules/widgets/eu-cookie-law/eu-cookie-law.js'
+ ),
+ array( 'jquery' ),
+ '20180522',
+ true
+ );
+ }
+
+ /**
+ * Return an associative array of default values.
+ *
+ * These values are used in new widgets.
+ *
+ * @return array Default values for the widget options.
+ */
+ public function defaults() {
+ return array(
+ 'hide' => $this->hide_options[0],
+ 'hide-timeout' => 30,
+ 'consent-expiration' => 180,
+ 'text' => $this->text_options[0],
+ 'customtext' => '',
+ 'color-scheme' => $this->color_scheme_options[0],
+ 'policy-url' => get_option( 'wp_page_for_privacy_policy' ) ? $this->policy_url_options[1] : $this->policy_url_options[0],
+ 'default-policy-url' => 'https://automattic.com/cookies/',
+ 'custom-policy-url' => get_option( 'wp_page_for_privacy_policy' ) ? get_permalink( (int) get_option( 'wp_page_for_privacy_policy' ) ) : '',
+ 'position' => $this->position_options[0],
+ 'policy-link-text' => esc_html__( 'Cookie Policy', 'jetpack' ),
+ 'button' => esc_html__( 'Close and accept', 'jetpack' ),
+ 'default-text' => esc_html__( "Privacy & Cookies: This site uses cookies. By continuing to use this website, you agree to their use. \r\nTo find out more, including how to control cookies, see here:", 'jetpack' ),
+ );
+ }
+
+ /**
+ * Front-end display of the widget.
+ *
+ * @param array $args Widget arguments.
+ * @param array $instance Saved values from database.
+ */
+ public function widget( $args, $instance ) {
+ /**
+ * Filters the display of the EU Cookie Law widget.
+ *
+ * @since 6.1.1
+ *
+ * @param bool true Should the EU Cookie Law widget be disabled. Default to false.
+ */
+ if ( apply_filters( 'jetpack_disable_eu_cookie_law_widget', false ) ) {
+ return;
+ }
+
+ $instance = wp_parse_args( $instance, $this->defaults() );
+
+ $classes = array();
+ $classes['hide'] = 'hide-on-' . esc_attr( $instance['hide'] );
+ if ( 'negative' === $instance['color-scheme'] ) {
+ $classes['negative'] = 'negative';
+ }
+
+ if ( 'top' === $instance['position'] ) {
+ $classes['top'] = 'top';
+ }
+
+ if ( Jetpack::is_module_active( 'wordads' ) ) {
+ $classes['ads'] = 'ads-active';
+ $classes['hide'] = 'hide-on-button';
+ }
+
+ echo $args['before_widget'];
+ require( dirname( __FILE__ ) . '/eu-cookie-law/widget.php' );
+ echo $args['after_widget'];
+ /** This action is already documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'eu_cookie_law' );
+ }
+
+ /**
+ * Back-end widget form.
+ *
+ * @param array $instance Previously saved values from database.
+ */
+ public function form( $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults() );
+ if ( Jetpack::is_module_active( 'wordads' ) ) {
+ $instance['hide'] = 'button';
+ }
+
+ wp_enqueue_script(
+ 'eu-cookie-law-widget-admin',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/widgets/eu-cookie-law/eu-cookie-law-admin.min.js',
+ 'modules/widgets/eu-cookie-law/eu-cookie-law-admin.js'
+ ),
+ array( 'jquery' ),
+ 20180417
+ );
+
+ require( dirname( __FILE__ ) . '/eu-cookie-law/form.php' );
+ }
+
+ /**
+ * Sanitize widget form values as they are saved.
+ *
+ * @param array $new_instance Values just sent to be saved.
+ * @param array $old_instance Previously saved values from database.
+ * @return array Updated safe values to be saved.
+ */
+ public function update( $new_instance, $old_instance ) {
+ $instance = array();
+ $defaults = $this->defaults();
+
+ $instance['hide'] = $this->filter_value( isset( $new_instance['hide'] ) ? $new_instance['hide'] : '', $this->hide_options );
+ $instance['text'] = $this->filter_value( isset( $new_instance['text'] ) ? $new_instance['text'] : '', $this->text_options );
+ $instance['color-scheme'] = $this->filter_value( isset( $new_instance['color-scheme'] ) ? $new_instance['color-scheme'] : '', $this->color_scheme_options );
+ $instance['policy-url'] = $this->filter_value( isset( $new_instance['policy-url'] ) ? $new_instance['policy-url'] : '', $this->policy_url_options );
+ $instance['position'] = $this->filter_value( isset( $new_instance['position'] ) ? $new_instance['position'] : '', $this->position_options );
+
+ if ( isset( $new_instance['hide-timeout'] ) ) {
+ // Time can be a value between 3 and 1000 seconds.
+ $instance['hide-timeout'] = min( 1000, max( 3, intval( $new_instance['hide-timeout'] ) ) );
+ }
+
+ if ( isset( $new_instance['consent-expiration'] ) ) {
+ // Time can be a value between 1 and 365 days.
+ $instance['consent-expiration'] = min( 365, max( 1, intval( $new_instance['consent-expiration'] ) ) );
+ }
+
+ if ( isset( $new_instance['customtext'] ) ) {
+ $instance['customtext'] = mb_substr( wp_kses( $new_instance['customtext'], array() ), 0, 4096 );
+ } else {
+ $instance['text'] = $this->text_options[0];
+ }
+
+ if ( isset( $new_instance['policy-url'] ) ) {
+ $instance['policy-url'] = 'custom' === $new_instance['policy-url']
+ ? 'custom'
+ : 'default';
+ } else {
+ $instance['policy-url'] = $this->policy_url_options[0];
+ }
+
+ if ( 'custom' === $instance['policy-url'] && isset( $new_instance['custom-policy-url'] ) ) {
+ $instance['custom-policy-url'] = esc_url( $new_instance['custom-policy-url'], array( 'http', 'https' ) );
+
+ if ( strlen( $instance['custom-policy-url'] ) < 10 ) {
+ unset( $instance['custom-policy-url'] );
+ global $wp_customize;
+ if ( ! isset( $wp_customize ) ) {
+ $instance['policy-url'] = $this->policy_url_options[0];
+ }
+ }
+ }
+
+ if ( isset( $new_instance['policy-link-text'] ) ) {
+ $instance['policy-link-text'] = trim( mb_substr( wp_kses( $new_instance['policy-link-text'], array() ), 0, 100 ) );
+ }
+
+ if ( empty( $instance['policy-link-text'] ) || $instance['policy-link-text'] == $defaults['policy-link-text'] ) {
+ unset( $instance['policy-link-text'] );
+ }
+
+ if ( isset( $new_instance['button'] ) ) {
+ $instance['button'] = trim( mb_substr( wp_kses( $new_instance['button'], array() ), 0, 100 ) );
+ }
+
+ if ( empty( $instance['button'] ) || $instance['button'] == $defaults['button'] ) {
+ unset( $instance['button'] );
+ }
+
+ // Show the banner again if a setting has been changed.
+ setcookie( self::$cookie_name, '', time() - 86400, '/' );
+
+ return $instance;
+ }
+
+ /**
+ * Check if the value is allowed and not empty.
+ *
+ * @param string $value Value to check.
+ * @param array $allowed Array of allowed values.
+ *
+ * @return string $value if pass the check or first value from allowed values.
+ */
+ function filter_value( $value, $allowed = array() ) {
+ $allowed = (array) $allowed;
+ if ( empty( $value ) || ( ! empty( $allowed ) && ! in_array( $value, $allowed ) ) ) {
+ $value = $allowed[0];
+ }
+ return $value;
+ }
+ }
+
+ // Register Jetpack_EU_Cookie_Law_Widget widget.
+ function jetpack_register_eu_cookie_law_widget() {
+ register_widget( 'Jetpack_EU_Cookie_Law_Widget' );
+ };
+
+ add_action( 'widgets_init', 'jetpack_register_eu_cookie_law_widget' );
+}
diff --git a/plugins/jetpack/modules/widgets/eu-cookie-law/eu-cookie-law-admin.js b/plugins/jetpack/modules/widgets/eu-cookie-law/eu-cookie-law-admin.js
new file mode 100644
index 00000000..6d3dd42e
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/eu-cookie-law/eu-cookie-law-admin.js
@@ -0,0 +1,32 @@
+/* eslint no-var: 0 */
+
+( function( $ ) {
+ var $document = $( document );
+
+ $document.on( 'ready', function() {
+ var maybeShowNotice = function( e, policyUrl ) {
+ var $policyUrl = $( policyUrl || this ).closest( '.eu-cookie-law-widget-policy-url' );
+
+ if ( $policyUrl.find( 'input[type="radio"][value="default"]' ).is( ':checked' ) ) {
+ $policyUrl.find( '.notice.default-policy' ).css( 'display', 'block' );
+ $policyUrl.find( '.notice.custom-policy' ).hide();
+ } else {
+ $policyUrl.find( '.notice.default-policy' ).hide();
+ $policyUrl.find( '.notice.custom-policy' ).css( 'display', 'block' );
+ }
+ };
+
+ $document.on(
+ 'click',
+ '.eu-cookie-law-widget-policy-url input[type="radio"]',
+ maybeShowNotice
+ );
+ $document.on( 'widget-updated widget-added', function( e, widget ) {
+ var widgetId = $( widget ).attr( 'id' );
+ if ( widgetId.indexOf( 'eu_cookie_law_widget' ) !== -1 ) {
+ maybeShowNotice( null, $( '#' + widgetId + ' .eu-cookie-law-widget-policy-url' ) );
+ }
+ } );
+ $( '.eu-cookie-law-widget-policy-url' ).each( maybeShowNotice );
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/widgets/eu-cookie-law/eu-cookie-law.js b/plugins/jetpack/modules/widgets/eu-cookie-law/eu-cookie-law.js
new file mode 100644
index 00000000..7989ee69
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/eu-cookie-law/eu-cookie-law.js
@@ -0,0 +1,80 @@
+( function( $ ) {
+ var cookieValue = document.cookie.replace(
+ /(?:(?:^|.*;\s*)eucookielaw\s*\=\s*([^;]*).*$)|^.*$/,
+ '$1'
+ ),
+ overlay = $( '#eu-cookie-law' ),
+ initialScrollPosition,
+ scrollFunction;
+
+ if ( overlay.hasClass( 'top' ) ) {
+ $( '.widget_eu_cookie_law_widget' ).addClass( 'top' );
+ }
+
+ if ( overlay.hasClass( 'ads-active' ) ) {
+ var adsCookieValue = document.cookie.replace(
+ /(?:(?:^|.*;\s*)personalized-ads-consent\s*\=\s*([^;]*).*$)|^.*$/,
+ '$1'
+ );
+ if ( '' !== cookieValue && '' !== adsCookieValue ) {
+ overlay.remove();
+ }
+ } else if ( '' !== cookieValue ) {
+ overlay.remove();
+ }
+
+ $( '.widget_eu_cookie_law_widget' )
+ .appendTo( 'body' )
+ .fadeIn();
+
+ overlay.find( 'form' ).on( 'submit', accept );
+
+ if ( overlay.hasClass( 'hide-on-scroll' ) ) {
+ initialScrollPosition = $( window ).scrollTop();
+ scrollFunction = function() {
+ if ( Math.abs( $( window ).scrollTop() - initialScrollPosition ) > 50 ) {
+ accept();
+ }
+ };
+ $( window ).on( 'scroll', scrollFunction );
+ } else if ( overlay.hasClass( 'hide-on-time' ) ) {
+ setTimeout( accept, overlay.data( 'hide-timeout' ) * 1000 );
+ }
+
+ var accepted = false;
+ function accept( event ) {
+ if ( accepted ) {
+ return;
+ }
+ accepted = true;
+
+ if ( event && event.preventDefault ) {
+ event.preventDefault();
+ }
+
+ if ( overlay.hasClass( 'hide-on-scroll' ) ) {
+ $( window ).off( 'scroll', scrollFunction );
+ }
+
+ var expireTime = new Date();
+ expireTime.setTime(
+ expireTime.getTime() + overlay.data( 'consent-expiration' ) * 24 * 60 * 60 * 1000
+ );
+
+ document.cookie =
+ 'eucookielaw=' + expireTime.getTime() + ';path=/;expires=' + expireTime.toGMTString();
+ if ( overlay.hasClass( 'ads-active' ) && overlay.hasClass( 'hide-on-button' ) ) {
+ document.cookie =
+ 'personalized-ads-consent=' +
+ expireTime.getTime() +
+ ';path=/;expires=' +
+ expireTime.toGMTString();
+ }
+
+ overlay.fadeOut( 400, function() {
+ overlay.remove();
+ var widgetSection = document.querySelector( '.widget.widget_eu_cookie_law_widget' );
+ widgetSection.parentNode.removeChild( widgetSection );
+ } );
+ }
+} )( jQuery );
diff --git a/plugins/jetpack/modules/widgets/eu-cookie-law/form.php b/plugins/jetpack/modules/widgets/eu-cookie-law/form.php
new file mode 100644
index 00000000..7b00877b
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/eu-cookie-law/form.php
@@ -0,0 +1,277 @@
+<p>
+ <strong>
+ <?php esc_html_e( 'Banner text', 'jetpack' ); ?>
+ </strong>
+ <ul>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['text'], 'default' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'text' ) ); ?>"
+ type="radio"
+ value="default"
+ />
+ <?php esc_html_e( 'Default', 'jetpack' ); ?>
+ </label>
+ </li>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['text'], 'custom' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'text' ) ); ?>"
+ type="radio"
+ value="custom"
+ />
+ <?php esc_html_e( 'Custom:', 'jetpack' ); ?>
+ </label>
+ </li>
+ </ul>
+ <textarea
+ class="widefat"
+ name="<?php echo esc_attr( $this->get_field_name( 'customtext' ) ); ?>"
+ placeholder="<?php echo esc_attr( $instance['default-text'] ); ?>"
+ ><?php echo esc_html( $instance['customtext'] ); ?></textarea>
+</p>
+
+<hr />
+
+<p>
+ <strong>
+ <?php esc_html_e( 'Privacy Policy Link', 'jetpack' ); ?>
+ </strong>
+ <ul class="eu-cookie-law-widget-policy-url">
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['policy-url'], 'default' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'policy-url' ) ); ?>"
+ type="radio"
+ value="default"
+ />
+ <?php esc_html_e( 'Default', 'jetpack' ); ?>
+ </label>
+ </li>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['policy-url'], 'custom' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'policy-url' ) ); ?>"
+ type="radio"
+ value="custom"
+ />
+ <?php esc_html_e( 'Custom URL:', 'jetpack' ); ?>
+ </label>
+ <input
+ class="widefat"
+ name="<?php echo esc_attr( $this->get_field_name( 'custom-policy-url' ) ); ?>"
+ placeholder="<?php echo esc_url( $instance['default-policy-url'] ); ?>"
+ style="margin-top: .5em;"
+ type="text"
+ value="<?php echo esc_url( $instance['custom-policy-url'] ); ?>"
+ />
+ <span class="notice notice-warning default-policy" style="display: none;">
+ <span style="display: block; margin: .5em 0;">
+ <strong><?php esc_html_e( 'Caution:', 'jetpack' ); ?></strong>
+ <?php esc_html_e( 'The default policy URL only covers cookies set by Jetpack. If you’re running other plugins, custom cookies, or third-party tracking technologies, you should create and link to your own cookie statement.', 'jetpack' ); ?>
+ </span>
+ </span>
+ <?php if ( Jetpack::is_module_active( 'wordads' ) ) : ?>
+ <span class="notice notice-warning custom-policy" style="display: none;">
+ <span style="display: block; margin: .5em 0;">
+ <strong><?php esc_html_e( 'Caution:', 'jetpack' ); ?></strong>
+ <?php echo sprintf(
+ __( 'For GDPR compliance, please make sure your policy contains <a href="%s" target="_blank">privacy information relating to Jetpack Ads</a>.', 'jetpack' ),
+ esc_url( 'https://jetpack.com/support/ads/#privacy' )
+ ); ?>
+ </span>
+ </span>
+ <?php endif; ?>
+ </li>
+ </ul>
+</p>
+
+<p>
+ <strong>
+ <?php esc_html_e( 'Link text', 'jetpack' ); ?>
+ </strong>
+ <label>
+ <input
+ class="widefat"
+ name="<?php echo $this->get_field_name( 'policy-link-text' ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $instance['policy-link-text'] ); ?>"
+ />
+ </label>
+</p>
+
+<hr />
+
+<p>
+ <strong>
+ <?php esc_html_e( 'Button text', 'jetpack' ); ?>
+ </strong>
+ <label>
+ <input
+ class="widefat"
+ name="<?php echo $this->get_field_name( 'button' ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $instance['button'] ); ?>"
+ />
+ </label>
+</p>
+
+<hr />
+
+<p>
+ <strong>
+ <?php _ex( 'Capture consent & hide the banner', 'action', 'jetpack' ); ?>
+ </strong>
+ <ul>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['hide'], 'button' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'hide' ) ); ?>"
+ type="radio"
+ value="button"
+ <?php echo Jetpack::is_module_active( 'wordads' ) ? 'disabled' : ''; ?>
+ />
+ <?php esc_html_e( 'after the user clicks the dismiss button', 'jetpack' ); ?>
+ </label>
+ </li>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['hide'], 'scroll' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'hide' ) ); ?>"
+ type="radio"
+ value="scroll"
+ <?php echo Jetpack::is_module_active( 'wordads' ) ? 'disabled' : ''; ?>
+ />
+ <?php esc_html_e( 'after the user scrolls the page', 'jetpack' ); ?>
+ </label>
+ </li>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['hide'], 'time' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'hide' ) ); ?>"
+ type="radio"
+ value="time"
+ <?php echo Jetpack::is_module_active( 'wordads' ) ? 'disabled' : ''; ?>
+ />
+ <?php esc_html_e( 'after this amount of time', 'jetpack' ); ?>
+ </label>
+ <input
+ max="1000"
+ min="3"
+ name="<?php echo esc_attr( $this->get_field_name( 'hide-timeout' ) ); ?>"
+ style="padding: 3px 5px; width: 3em;"
+ type="number"
+ value="<?php echo esc_attr( $instance['hide-timeout'] ); ?>"
+ />
+ <?php esc_html_e( 'seconds', 'jetpack' ); ?>
+ </li>
+ </ul>
+ <?php if ( Jetpack::is_module_active( 'wordads' ) ) : ?>
+ <span class="notice notice-warning" style="display: block;">
+ <span style="display: block; margin: .5em 0;">
+ <?php esc_html_e( 'Visitors must provide consent by clicking the dismiss button when Jetpack Ads is turned on.', 'jetpack' ); ?>
+ </span>
+ </span>
+ <?php endif; ?>
+</p>
+
+<hr />
+
+<p>
+ <strong>
+ <?php _ex( 'Consent expires after', 'action', 'jetpack' ); ?>
+ </strong>
+ <ul>
+ <li>
+ <input
+ max="365"
+ min="1"
+ name="<?php echo esc_attr( $this->get_field_name( 'consent-expiration' ) ); ?>"
+ style="padding: 3px 5px; width: 3.75em;"
+ type="number"
+ value="<?php echo esc_attr( $instance['consent-expiration'] ); ?>"
+ />
+ <?php esc_html_e( 'days', 'jetpack' ); ?>
+ </li>
+ </ul>
+</p>
+
+<hr />
+
+<p>
+ <strong>
+ <?php _e( 'Color scheme', 'jetpack' ); ?>
+ </strong>
+ <ul>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['color-scheme'], 'default' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'color-scheme' ) ); ?>"
+ type="radio"
+ value="default"
+ />
+ <?php esc_html_e( 'Light', 'jetpack' ); ?>
+ </label>
+ </li>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['color-scheme'], 'negative' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'color-scheme' ) ); ?>"
+ type="radio"
+ value="negative"
+ />
+ <?php esc_html_e( 'Dark', 'jetpack' ); ?>
+ </label>
+ </li>
+ </ul>
+</p>
+
+<hr />
+
+<p>
+ <strong>
+ <?php _e( 'Position', 'jetpack' ); ?>
+ </strong>
+ <ul>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['position'], 'bottom' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'position' ) ); ?>"
+ type="radio"
+ value="bottom"
+ />
+ <?php esc_html_e( 'Bottom', 'jetpack' ); ?>
+ </label>
+ </li>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['position'], 'top' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'position' ) ); ?>"
+ type="radio"
+ value="top"
+ />
+ <?php esc_html_e( 'Top', 'jetpack' ); ?>
+ </label>
+ </li>
+ </ul>
+</p>
+
+<hr />
+
+<p class="small">
+ <?php esc_html_e( 'It is your own responsibility to ensure that your site complies with the relevant laws.', 'jetpack' ); ?>
+ <a href="https://jetpack.com/support/extra-sidebar-widgets/eu-cookie-law-widget/">
+ <?php esc_html_e( 'Click here for more information', 'jetpack' ); ?>
+ </a>
+</p>
diff --git a/plugins/jetpack/modules/widgets/eu-cookie-law/style.css b/plugins/jetpack/modules/widgets/eu-cookie-law/style.css
new file mode 100644
index 00000000..07d5a9f6
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/eu-cookie-law/style.css
@@ -0,0 +1,105 @@
+.widget_eu_cookie_law_widget {
+ border: none;
+ bottom: 1em;
+ display: none;
+ left: 1em;
+ margin: 0;
+ padding: 0;
+ position: fixed;
+ right: 1em;
+ width: auto;
+ z-index: 50001;
+}
+
+.widget_eu_cookie_law_widget.widget.top {
+ bottom: auto;
+ top: 1em;
+}
+
+.admin-bar .widget_eu_cookie_law_widget.widget.top {
+ top: 3em;
+}
+
+#eu-cookie-law {
+ background-color: #fff;
+ border: 1px solid #dedede;
+ color: #2e4467;
+ font-size: 12px;
+ line-height: 1.5;
+ overflow: hidden;
+ padding: 6px 6px 6px 15px;
+ position: relative;
+}
+
+#eu-cookie-law a,
+#eu-cookie-law a:active,
+#eu-cookie-law a:visited {
+ color: inherit;
+ cursor: inherit;
+ text-decoration: underline;
+}
+
+#eu-cookie-law a:hover {
+ cursor: pointer;
+ text-decoration: none;
+}
+
+#eu-cookie-law.negative {
+ background-color: #000;
+ border: none;
+ color: #fff;
+}
+
+/**
+ * Using a highly-specific rule to make sure that certain form styles
+ * will be reset
+ */
+#eu-cookie-law form {
+ margin-bottom: 0;
+ position: static;
+}
+
+/**
+ * Using a highly-specific rule to make sure that all button styles
+ * will be reset
+ */
+#eu-cookie-law input,
+#eu-cookie-law input:hover,
+#eu-cookie-law input:focus {
+ background: #f3f3f3;
+ border: 1px solid #dedede;
+ border-radius: 4px;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ color: #2e4453;
+ cursor: pointer;
+ display: inline;
+ float: right;
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: inherit;
+ line-height: inherit;
+ margin: 0 0 0 5%;
+ padding: 8px 12px;
+ position: static;
+ text-transform: none;
+}
+
+#eu-cookie-law.negative input,
+#eu-cookie-law.negative input:hover,
+#eu-cookie-law.negative input:focus {
+ background: #282828;
+ border-color: #535353;
+ color: #fff;
+}
+
+@media ( max-width: 600px ) {
+ #eu-cookie-law {
+ padding-bottom: 55px;
+ }
+ #eu-cookie-law input.accept {
+ bottom: 8px;
+ position: absolute;
+ right: 8px;
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/eu-cookie-law/widget.php b/plugins/jetpack/modules/widgets/eu-cookie-law/widget.php
new file mode 100644
index 00000000..cd016a3e
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/eu-cookie-law/widget.php
@@ -0,0 +1,25 @@
+<div
+ class="<?php echo implode( ' ', $classes ); ?>"
+ data-hide-timeout="<?php echo intval( $instance['hide-timeout'] ); ?>"
+ data-consent-expiration="<?php echo intval( $instance['consent-expiration'] ); ?>"
+ id="eu-cookie-law"
+>
+ <form method="post">
+ <input type="submit" value="<?php echo esc_attr( $instance['button'] ); ?>" class="accept" />
+ </form>
+
+ <?php if ( 'default' == $instance['text'] || empty( $instance['customtext'] ) ) {
+ echo nl2br( $instance['default-text'] );
+ } else {
+ echo nl2br( esc_html( $instance['customtext'] ) );
+ } ?>
+
+ <a href="<?php
+ $policy_link_text = 'default' === $instance['policy-url'] || empty( $instance['custom-policy-url'] )
+ ? $instance['default-policy-url']
+ : $instance['custom-policy-url'];
+ echo esc_url( $policy_link_text );
+ ?>" >
+ <?php echo esc_html( $instance['policy-link-text'] ); ?>
+ </a>
+</div>
diff --git a/plugins/jetpack/modules/widgets/facebook-likebox.php b/plugins/jetpack/modules/widgets/facebook-likebox.php
new file mode 100644
index 00000000..5fbc23e0
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/facebook-likebox.php
@@ -0,0 +1,309 @@
+<?php
+
+/**
+ * Register the widget for use in Appearance -> Widgets
+ */
+add_action( 'widgets_init', 'jetpack_facebook_likebox_init' );
+
+function jetpack_facebook_likebox_init() {
+ register_widget( 'WPCOM_Widget_Facebook_LikeBox' );
+}
+
+/**
+ * Facebook Page Plugin (formerly known as the Like Box)
+ * Display a Facebook Page Plugin as a widget (replaces the old like box plugin)
+ * https://developers.facebook.com/docs/plugins/page-plugin
+ */
+class WPCOM_Widget_Facebook_LikeBox extends WP_Widget {
+
+ private $default_height = 580;
+ private $default_width = 340;
+ private $max_width = 500;
+ private $min_width = 180;
+ private $max_height = 9999;
+ private $min_height = 130;
+
+ function __construct() {
+ parent::__construct(
+ 'facebook-likebox',
+ /**
+ * Filter the name of a widget included in the Extra Sidebar Widgets module.
+ *
+ * @module widgets
+ *
+ * @since 2.1.2
+ *
+ * @param string $widget_name Widget title.
+ */
+ apply_filters( 'jetpack_widget_name', __( 'Facebook Page Plugin', 'jetpack' ) ),
+ array(
+ 'classname' => 'widget_facebook_likebox',
+ 'description' => __( 'Use the Facebook Page Plugin to connect visitors to your Facebook Page', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+
+ if ( is_active_widget( false, false, $this->id_base ) || is_active_widget( false, false, 'monster' ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+ }
+ }
+
+ /**
+ * Enqueue scripts.
+ */
+ public function enqueue_scripts() {
+ wp_enqueue_script( 'jetpack-facebook-embed' );
+ wp_enqueue_style( 'jetpack_facebook_likebox', plugins_url( 'facebook-likebox/style.css', __FILE__ ) );
+ wp_style_add_data( 'jetpack_facebook_likebox', 'jetpack-inline', true );
+ }
+
+ function widget( $args, $instance ) {
+ extract( $args );
+
+ $like_args = $this->normalize_facebook_args( $instance['like_args'] );
+
+ if ( empty( $like_args['href'] ) || ! $this->is_valid_facebook_url( $like_args['href'] ) ) {
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ echo $before_widget;
+ echo '<p>' . sprintf( __( 'It looks like your Facebook URL is incorrectly configured. Please check it in your <a href="%s">widget settings</a>.', 'jetpack' ), admin_url( 'widgets.php' ) ) . '</p>';
+ echo $after_widget;
+ }
+ echo '<!-- Invalid Facebook Page URL -->';
+ return;
+ }
+
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', $instance['title'] );
+ $page_url = set_url_scheme( $like_args['href'], 'https' );
+
+ $like_args['show_faces'] = (bool) $like_args['show_faces'] ? 'true' : 'false';
+ $like_args['stream'] = (bool) $like_args['stream'] ? 'true' : 'false';
+ $like_args['cover'] = (bool) $like_args['cover'] ? 'false' : 'true';
+
+ echo $before_widget;
+
+ if ( ! empty( $title ) ) :
+ echo $before_title;
+
+ $likebox_widget_title = '<a href="' . esc_url( $page_url ) . '">' . esc_html( $title ) . '</a>';
+
+ /**
+ * Filter Facebook Likebox's widget title.
+ *
+ * @module widgets
+ *
+ * @since 3.3.0
+ *
+ * @param string $likebox_widget_title Likebox Widget title (including a link to the Page URL).
+ * @param string $title Widget title as set in the widget settings.
+ * @param string $page_url Facebook Page URL.
+ */
+ echo apply_filters( 'jetpack_facebook_likebox_title', $likebox_widget_title, $title, $page_url );
+
+ echo $after_title;
+ endif;
+
+ ?>
+ <div id="fb-root"></div>
+ <div class="fb-page" data-href="<?php echo esc_url( $page_url ); ?>" data-width="<?php echo intval( $like_args['width'] ); ?>" data-height="<?php echo intval( $like_args['height'] ); ?>" data-hide-cover="<?php echo esc_attr( $like_args['cover'] ); ?>" data-show-facepile="<?php echo esc_attr( $like_args['show_faces'] ); ?>" data-show-posts="<?php echo esc_attr( $like_args['stream'] ); ?>">
+ <div class="fb-xfbml-parse-ignore"><blockquote cite="<?php echo esc_url( $page_url ); ?>"><a href="<?php echo esc_url( $page_url ); ?>"><?php echo esc_html( $title ); ?></a></blockquote></div>
+ </div>
+ <?php
+ wp_enqueue_script( 'jetpack-facebook-embed' );
+ echo $after_widget;
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'facebook-likebox' );
+ }
+
+ function update( $new_instance, $old_instance ) {
+ $instance = array(
+ 'title' => '',
+ 'like_args' => $this->get_default_args(),
+ );
+
+ $instance['title'] = trim( strip_tags( stripslashes( $new_instance['title'] ) ) );
+
+ // Set up widget values
+ $instance['like_args'] = array(
+ 'href' => trim( strip_tags( stripslashes( $new_instance['href'] ) ) ),
+ 'width' => (int) $new_instance['width'],
+ 'height' => (int) $new_instance['height'],
+ 'show_faces' => isset( $new_instance['show_faces'] ),
+ 'stream' => isset( $new_instance['stream'] ),
+ 'cover' => isset( $new_instance['cover'] ),
+ );
+
+ $instance['like_args'] = $this->normalize_facebook_args( $instance['like_args'] );
+
+ return $instance;
+ }
+
+ function form( $instance ) {
+ $instance = wp_parse_args(
+ (array) $instance, array(
+ 'title' => '',
+ 'like_args' => $this->get_default_args(),
+ )
+ );
+ $like_args = $this->normalize_facebook_args( $instance['like_args'] );
+ ?>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
+ <?php _e( 'Title', 'jetpack' ); ?>
+ <input type="text" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" value="<?php echo esc_attr( $instance['title'] ); ?>" class="widefat" />
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'href' ) ); ?>">
+ <?php _e( 'Facebook Page URL', 'jetpack' ); ?>
+ <input type="text" name="<?php echo esc_attr( $this->get_field_name( 'href' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'href' ) ); ?>" value="<?php echo esc_url( $like_args['href'] ); ?>" class="widefat" />
+ <br />
+ <small><?php _e( 'The widget only works with Facebook Pages.', 'jetpack' ); ?></small>
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'width' ) ); ?>">
+ <?php _e( 'Width in pixels', 'jetpack' ); ?>
+ <input type="number" class="smalltext" min="<?php echo esc_attr( $this->min_width ); ?>" max="<?php echo esc_attr( $this->max_width ); ?>" maxlength="3" name="<?php echo esc_attr( $this->get_field_name( 'width' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'width' ) ); ?>" value="<?php echo esc_attr( $like_args['width'] ); ?>" style="text-align: center;" />
+ <small><?php echo sprintf( __( 'Minimum: %s', 'jetpack' ), $this->min_width ); ?> / <?php echo sprintf( __( 'Maximum: %s', 'jetpack' ), $this->max_width ); ?></small>
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'height' ) ); ?>">
+ <?php _e( 'Height in pixels', 'jetpack' ); ?>
+ <input type="number" class="smalltext" min="<?php echo esc_attr( $this->min_height ); ?>" max="<?php echo esc_attr( $this->max_height ); ?>" maxlength="3" name="<?php echo esc_attr( $this->get_field_name( 'height' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'height' ) ); ?>" value="<?php echo esc_attr( $like_args['height'] ); ?>" style="text-align: center;" />
+ <small><?php echo sprintf( __( 'Minimum: %s', 'jetpack' ), $this->min_height ); ?> / <?php echo sprintf( __( 'Maximum: %s', 'jetpack' ), $this->max_height ); ?></small>
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'show_faces' ) ); ?>">
+ <input type="checkbox" name="<?php echo esc_attr( $this->get_field_name( 'show_faces' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'show_faces' ) ); ?>" <?php checked( $like_args['show_faces'] ); ?> />
+ <?php _e( 'Show Faces', 'jetpack' ); ?>
+ <br />
+ <small><?php _e( 'Show profile photos in the plugin.', 'jetpack' ); ?></small>
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'stream' ) ); ?>">
+ <input type="checkbox" name="<?php echo esc_attr( $this->get_field_name( 'stream' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'stream' ) ); ?>" <?php checked( $like_args['stream'] ); ?> />
+ <?php _e( 'Show Stream', 'jetpack' ); ?>
+ <br />
+ <small><?php _e( 'Show Page Posts.', 'jetpack' ); ?></small>
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'cover' ) ); ?>">
+ <input type="checkbox" name="<?php echo esc_attr( $this->get_field_name( 'cover' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'cover' ) ); ?>" <?php checked( $like_args['cover'] ); ?> />
+ <?php _e( 'Show Cover Photo', 'jetpack' ); ?>
+ <br />
+ </label>
+ </p>
+
+ <?php
+ }
+
+ function get_default_args() {
+ $defaults = array(
+ 'href' => '',
+ 'width' => $this->default_width,
+ 'height' => $this->default_height,
+ 'show_faces' => 'true',
+ 'stream' => '',
+ 'cover' => 'true',
+ );
+
+ /**
+ * Filter Facebook Likebox default options.
+ *
+ * @module widgets
+ *
+ * @since 1.3.1
+ *
+ * @param array $defaults Array of default options.
+ */
+ return apply_filters( 'jetpack_facebook_likebox_defaults', $defaults );
+ }
+
+ function normalize_facebook_args( $args ) {
+ $args = wp_parse_args( (array) $args, $this->get_default_args() );
+
+ // Validate the Facebook Page URL
+ if ( $this->is_valid_facebook_url( $args['href'] ) ) {
+ $temp = explode( '?', $args['href'] );
+ $args['href'] = str_replace( array( 'http://facebook.com', 'https://facebook.com' ), array( 'http://www.facebook.com', 'https://www.facebook.com' ), $temp[0] );
+ } else {
+ $args['href'] = '';
+ }
+
+ $args['width'] = $this->normalize_int_value( (int) $args['width'], $this->default_width, $this->max_width, $this->min_width );
+ $args['height'] = $this->normalize_int_value( (int) $args['height'], $this->default_height, $this->max_height, $this->min_height );
+ $args['show_faces'] = (bool) $args['show_faces'];
+ $args['stream'] = (bool) $args['stream'];
+ $args['cover'] = (bool) $args['cover'];
+
+ // The height used to be dependent on other widget settings
+ // If the user changes those settings but doesn't customize the height,
+ // let's intelligently assign a new height.
+ if ( in_array( $args['height'], array( 580, 110, 432 ) ) ) {
+ if ( $args['show_faces'] && $args['stream'] ) {
+ $args['height'] = 580;
+ } elseif ( ! $args['show_faces'] && ! $args['stream'] ) {
+ $args['height'] = 130;
+ } else {
+ $args['height'] = 432;
+ }
+ }
+
+ return $args;
+ }
+
+ function is_valid_facebook_url( $url ) {
+ return ( false !== strpos( $url, 'facebook.com' ) ) ? true : false;
+ }
+
+ function normalize_int_value( $value, $default = 0, $max = 0, $min = 0 ) {
+ $value = (int) $value;
+
+ if ( $value > $max ) {
+ $value = $max;
+ } elseif ( $value < $min ) {
+ $value = $min;
+ }
+
+ return (int) $value;
+ }
+
+ function normalize_text_value( $value, $default = '', $allowed = array() ) {
+ $allowed = (array) $allowed;
+
+ if ( empty( $value ) || ( ! empty( $allowed ) && ! in_array( $value, $allowed ) ) ) {
+ $value = $default;
+ }
+
+ return $value;
+ }
+
+ /**
+ * @deprecated
+ */
+ function guess_locale_from_lang( $lang ) {
+ _deprecated_function( __METHOD__, '4.0.0', 'Jetpack::guess_locale_from_lang()' );
+ Jetpack::$instance->guess_locale_from_lang( $lang );
+ }
+
+ /**
+ * @deprecated
+ */
+ function get_locale() {
+ _deprecated_function( __METHOD__, '4.0.0', 'Jetpack::get_locale()' );
+ Jetpack::$instance->get_locale();
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/facebook-likebox/style.css b/plugins/jetpack/modules/widgets/facebook-likebox/style.css
new file mode 100644
index 00000000..d5d554ba
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/facebook-likebox/style.css
@@ -0,0 +1,3 @@
+.widget_facebook_likebox {
+ overflow: hidden;
+}
diff --git a/plugins/jetpack/modules/widgets/flickr.php b/plugins/jetpack/modules/widgets/flickr.php
new file mode 100644
index 00000000..a7867612
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/flickr.php
@@ -0,0 +1,218 @@
+<?php
+/**
+ * Disable direct access/execution to/of the widget code.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+if ( ! class_exists( 'Jetpack_Flickr_Widget' ) ) {
+ /**
+ * Flickr Widget
+ *
+ * Display your recent Flickr photos.
+ */
+ class Jetpack_Flickr_Widget extends WP_Widget {
+ /**
+ * Constructor.
+ */
+ function __construct() {
+ parent::__construct(
+ 'flickr',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', esc_html__( 'Flickr', 'jetpack' ) ),
+ array(
+ 'description' => esc_html__( 'Display your recent Flickr photos.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ ),
+ array()
+ );
+
+ if ( is_active_widget( false, false, $this->id_base ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_style' ) );
+ }
+ }
+
+ /**
+ * Enqueue style.
+ */
+ function enqueue_style() {
+ wp_enqueue_style( 'flickr-widget-style', plugins_url( 'flickr/style.css', __FILE__ ), array(), '20170405' );
+ }
+
+ /**
+ * Return an associative array of default values.
+ *
+ * These values are used in new widgets.
+ *
+ * @return array Default values for the widget options.
+ */
+ public function defaults() {
+ return array(
+ 'title' => esc_html__( 'Flickr Photos', 'jetpack' ),
+ 'items' => 4,
+ 'flickr_image_size' => 'thumbnail',
+ 'flickr_rss_url' => '',
+ );
+ }
+
+ /**
+ * Front-end display of the widget.
+ *
+ * @param array $args Widget arguments.
+ * @param array $instance Saved values from database.
+ */
+ public function widget( $args, $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults() );
+
+ $image_size_string = 'small' == $instance['flickr_image_size'] ? '_m.jpg' : '_t.jpg';
+
+ if ( ! empty( $instance['flickr_rss_url'] ) ) {
+
+ /*
+ * Parse the URL, and rebuild a URL that's sure to display images.
+ * Some Flickr Feeds do not display images by default.
+ */
+ $flickr_parameters = parse_url( htmlspecialchars_decode( $instance['flickr_rss_url'] ) );
+
+ // Is it a Flickr Feed.
+ if (
+ ! empty( $flickr_parameters['host'] )
+ && ! empty( $flickr_parameters['query'] )
+ && false !== strpos( $flickr_parameters['host'], 'flickr' )
+ ) {
+ parse_str( $flickr_parameters['query'], $vars );
+
+ // Do we have an ID in the feed? Let's continue.
+ if ( isset( $vars['id'] ) ) {
+
+ // Flickr Feeds can be used for groups or for individuals.
+ if (
+ ! empty( $flickr_parameters['path'] )
+ && false !== strpos( $flickr_parameters['path'], 'groups' )
+ ) {
+ $feed_url = 'https://api.flickr.com/services/feeds/groups_pool.gne';
+ } else {
+ $feed_url = 'https://api.flickr.com/services/feeds/photos_public.gne';
+ }
+
+ // Build our new RSS feed.
+ $rss_url = sprintf(
+ '%1$s?id=%2$s&format=rss_200_enc',
+ esc_url( $feed_url ),
+ esc_attr( $vars['id'] )
+ );
+ }
+ }
+ } // End if().
+
+ // Still no RSS feed URL? Get a default feed from Flickr to grab interesting photos.
+ if ( empty( $rss_url ) ) {
+ $rss_url = 'https://api.flickr.com/services/feeds/photos_interesting.gne?format=rss_200';
+ }
+
+ $rss = fetch_feed( $rss_url );
+
+ $photos = '';
+ if ( ! is_wp_error( $rss ) ) {
+ foreach ( $rss->get_items( 0, $instance['items'] ) as $photo ) {
+ switch ( $instance['flickr_image_size'] ) {
+ case 'thumbnail':
+ $src = $photo->get_enclosure()->get_thumbnail();
+ break;
+ case 'small':
+ $src = preg_match( '/src="(.*?)"/i', $photo->get_description(), $p );
+ $src = $p[1];
+ break;
+ case 'large':
+ $src = $photo->get_enclosure()->get_link();
+ break;
+ }
+
+ $photos .= '<a href="' . esc_url( $photo->get_permalink(), array( 'http', 'https' ) ) . '">';
+ $photos .= '<img src="' . esc_url( $src, array( 'http', 'https' ) ) . '" ';
+ $photos .= 'alt="' . esc_attr( $photo->get_title() ) . '" ';
+ $photos .= 'title="' . esc_attr( $photo->get_title() ) . '" ';
+ $photos .= ' /></a>';
+ }
+ if ( ! empty( $photos ) && class_exists( 'Jetpack_Photon' ) && Jetpack::is_module_active( 'photon' ) ) {
+ $photos = Jetpack_Photon::filter_the_content( $photos );
+ }
+
+ $flickr_home = $rss->get_link();
+ }
+
+ echo $args['before_widget'];
+ if ( empty( $photos ) ) {
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ printf(
+ '<p>%1$s<br />%2$s</p>',
+ esc_html__( 'There are no photos to display. Make sure your Flickr feed URL is correct, and that your pictures are publicly accessible.', 'jetpack' ),
+ esc_html__( '(Only admins can see this message)', 'jetpack' )
+ );
+ }
+ } else {
+ echo $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title'];
+ require( dirname( __FILE__ ) . '/flickr/widget.php' );
+ }
+ echo $args['after_widget'];
+ /** This action is already documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'flickr' );
+ }
+
+ /**
+ * Back-end widget form.
+ *
+ * @param array $instance Previously saved values from database.
+ */
+ public function form( $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults() );
+ require( dirname( __FILE__ ) . '/flickr/form.php' );
+ }
+
+ /**
+ * Sanitize widget form values as they are saved.
+ *
+ * @param array $new_instance Values just sent to be saved.
+ * @param array $old_instance Previously saved values from database.
+ * @return array Updated safe values to be saved.
+ */
+ public function update( $new_instance, $old_instance ) {
+ $instance = array();
+ $defaults = $this->defaults();
+
+ if ( isset( $new_instance['title'] ) ) {
+ $instance['title'] = wp_kses( $new_instance['title'], array() );
+ }
+
+ if ( isset( $new_instance['items'] ) ) {
+ $instance['items'] = intval( $new_instance['items'] );
+ }
+
+ if (
+ isset( $new_instance['flickr_image_size'] ) &&
+ in_array( $new_instance['flickr_image_size'], array( 'thumbnail', 'small', 'large' ) )
+ ) {
+ $instance['flickr_image_size'] = $new_instance['flickr_image_size'];
+ } else {
+ $instance['flickr_image_size'] = 'thumbnail';
+ }
+
+ if ( isset( $new_instance['flickr_rss_url'] ) ) {
+ $instance['flickr_rss_url'] = esc_url( $new_instance['flickr_rss_url'], array( 'http', 'https' ) );
+
+ if ( strlen( $instance['flickr_rss_url'] ) < 10 ) {
+ $instance['flickr_rss_url'] = '';
+ }
+ }
+
+ return $instance;
+ }
+ }
+
+ // Register Jetpack_Flickr_Widget widget.
+ function jetpack_register_flickr_widget() {
+ register_widget( 'Jetpack_Flickr_Widget' );
+ }
+ add_action( 'widgets_init', 'jetpack_register_flickr_widget' );
+}
diff --git a/plugins/jetpack/modules/widgets/flickr/form.php b/plugins/jetpack/modules/widgets/flickr/form.php
new file mode 100644
index 00000000..b08e7c4d
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/flickr/form.php
@@ -0,0 +1,93 @@
+<p>
+ <label>
+ <?php esc_html_e( 'Title:', 'jetpack' ); ?>
+ </label>
+ <input
+ class="widefat"
+ name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $instance['title'] ); ?>"
+ />
+</p>
+
+<p>
+ <label>
+ <?php esc_html_e( 'Flickr RSS URL:', 'jetpack' ); ?>
+ </label>
+ <input
+ class="widefat"
+ name="<?php echo esc_attr( $this->get_field_name( 'flickr_rss_url' ) ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $instance['flickr_rss_url'] ); ?>"
+ />
+</p>
+<p>
+ <small>
+ <?php esc_html_e( 'To find your Flickr RSS URL, go to your photostream, add "?details=1" to the URL, and hit enter. Scroll down until you see the RSS icon or the "Latest" link. Right-click on either options and copy the URL. Paste into the box above.', 'jetpack' ); ?>
+ </small>
+</p>
+<p>
+ <small>
+ <?php printf(
+ __( 'Leave the Flickr RSS URL field blank to display <a target="_blank" href="%s">interesting</a> Flickr photos.', 'jetpack' ),
+ 'http://www.flickr.com/explore/interesting'
+ ); ?>
+ </small>
+</p>
+
+<p>
+ <label>
+ <?php esc_html_e( 'How many photos would you like to display?', 'jetpack' ); ?>
+ </label>
+ <select name="<?php echo esc_attr( $this->get_field_name( 'items' ) ); ?>">
+ <?php for ( $i = 1; $i <= 10; ++$i ) { ?>
+ <option
+ <?php selected( $instance['items'], $i ); ?>
+ value="<?php echo $i; ?>"
+ >
+ <?php echo $i; ?>
+ </option>
+ <?php } ?>
+ </select>
+</p>
+
+<p>
+ <div>
+ <?php esc_html_e( 'What size photos would you like to display?', 'jetpack' ); ?>
+ </div>
+ <ul>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['flickr_image_size'], 'thumbnail' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'flickr_image_size' ) ); ?>"
+ type="radio"
+ value="thumbnail"
+ />
+ <?php esc_html_e( 'Thumbnail', 'jetpack' ); ?>
+ </label>
+ </li>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['flickr_image_size'], 'small' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'flickr_image_size' ) ); ?>"
+ type="radio"
+ value="small"
+ />
+ <?php esc_html_e( 'Medium', 'jetpack' ); ?>
+ </label>
+ </li>
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['flickr_image_size'], 'large' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'flickr_image_size' ) ); ?>"
+ type="radio"
+ value="large"
+ />
+ <?php esc_html_e( 'Large', 'jetpack' ); ?>
+ </label>
+ </li>
+ </ul>
+</p>
diff --git a/plugins/jetpack/modules/widgets/flickr/style.css b/plugins/jetpack/modules/widgets/flickr/style.css
new file mode 100644
index 00000000..689e240a
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/flickr/style.css
@@ -0,0 +1,16 @@
+.flickr-images {
+ text-align: center;
+}
+
+.flickr-size-thumbnail .flickr-images {
+ align-content: space-between;
+ align-items: center;
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: center;
+}
+
+.flickr-images img {
+ max-width: 100%;
+ margin: 5px;
+}
diff --git a/plugins/jetpack/modules/widgets/flickr/widget.php b/plugins/jetpack/modules/widgets/flickr/widget.php
new file mode 100644
index 00000000..0c45f3f0
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/flickr/widget.php
@@ -0,0 +1,13 @@
+<!-- Start of Flickr Widget -->
+<div class="flickr-wrapper flickr-size-<?php echo esc_attr( $instance['flickr_image_size'] ); ?>">
+ <div class="flickr-images">
+ <?php echo $photos; ?>
+ </div>
+
+ <?php if ( isset( $flickr_home ) ) { ?>
+ <a class="flickr-more" href="<?php echo esc_url( $flickr_home, array( 'http', 'https' ) ); ?>">
+ <?php esc_html_e( 'More Photos', 'jetpack' ); ?>
+ </a>
+ <?php } ?>
+</div>
+<!-- End of Flickr Widget -->
diff --git a/plugins/jetpack/modules/widgets/gallery.php b/plugins/jetpack/modules/widgets/gallery.php
new file mode 100644
index 00000000..8cb24d01
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gallery.php
@@ -0,0 +1,464 @@
+<?php
+
+/*
+Plugin Name: Gallery
+Description: Gallery widget
+Author: Automattic Inc.
+Version: 1.0
+Author URI: http://automattic.com
+*/
+
+class Jetpack_Gallery_Widget extends WP_Widget {
+ const THUMB_SIZE = 45;
+ const DEFAULT_WIDTH = 265;
+
+ protected $_instance_width;
+
+ public function __construct() {
+ $widget_ops = array(
+ 'classname' => 'widget-gallery',
+ 'description' => __( 'Display a photo gallery or slideshow', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ );
+
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
+
+ parent::__construct(
+ 'gallery',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Gallery', 'jetpack' ) ),
+ $widget_ops
+ );
+
+ if ( is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) );
+
+ if ( class_exists( 'Jetpack_Tiled_Gallery' ) ) {
+ add_action( 'wp_enqueue_scripts', array( 'Jetpack_Tiled_Gallery', 'default_scripts_and_styles' ) );
+ }
+
+ if ( class_exists( 'Jetpack_Slideshow_Shortcode' ) ) {
+ $slideshow = new Jetpack_Slideshow_Shortcode();
+ add_action( 'wp_enqueue_scripts', array( $slideshow, 'enqueue_scripts' ) );
+ }
+
+ if ( class_exists( 'Jetpack_Carousel' ) ) {
+ $carousel = new Jetpack_Carousel();
+ add_action( 'wp_enqueue_scripts', array( $carousel, 'enqueue_assets' ) );
+ }
+ }
+ }
+
+ /**
+ * @param array $args Display arguments including before_title, after_title, before_widget, and after_widget.
+ * @param array $instance The settings for the particular instance of the widget.
+ */
+ public function widget( $args, $instance ) {
+ $instance = wp_parse_args( (array) $instance, $this->defaults() );
+
+ $this->enqueue_frontend_scripts();
+
+ extract( $args );
+
+ $instance['attachments'] = $this->get_attachments( $instance );
+
+ $classes = array();
+
+ $classes[] = 'widget-gallery-' . $instance['type'];
+
+ // Due to a bug in the carousel plugin, carousels will be triggered for all tiled galleries that exist on a page
+ // with other tiled galleries, regardless of whether or not the widget was set to Carousel mode. The onClick selector
+ // is simply too broad, since it was not written with widgets in mind. This special class prevents that behavior, via
+ // an override handler in gallery.js
+ if ( 'carousel' != $instance['link'] && 'slideshow' != $instance['type'] ) {
+ $classes[] = 'no-carousel';
+ } else {
+ $classes[] = 'carousel';
+ }
+
+ $classes = implode( ' ', $classes );
+
+ if ( 'carousel' == $instance['link'] ) {
+ require_once plugin_dir_path( realpath( dirname( __FILE__ ) . '/../carousel/jetpack-carousel.php' ) ) . 'jetpack-carousel.php';
+
+ if ( class_exists( 'Jetpack_Carousel' ) ) {
+ // Create new carousel so we can use the enqueue_assets() method. Not ideal, but there is a decent amount
+ // of logic in that method that shouldn't be duplicated.
+ $carousel = new Jetpack_Carousel();
+
+ // First parameter is $output, which comes from filters, and causes bypass of the asset enqueuing. Passing null is correct.
+ $carousel->enqueue_assets( null );
+ }
+ }
+
+ echo $before_widget . "\n";
+
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', $instance['title'] );
+
+ if ( $title ) {
+ echo $before_title . esc_html( $title ) . $after_title . "\n";
+ }
+
+ echo '<div class="' . esc_attr( $classes ) . '">' . "\n";
+
+ $method = $instance['type'] . '_widget';
+
+ /**
+ * Allow the width of a gallery to be altered by themes or other code.
+ *
+ * @module widgets
+ *
+ * @since 2.5.0
+ *
+ * @param int self::DEFAULT_WIDTH Default gallery width. Default is 265.
+ * @param string $args Display arguments including before_title, after_title, before_widget, and after_widget.
+ * @param array $instance The settings for the particular instance of the widget.
+ */
+ $this->_instance_width = apply_filters( 'gallery_widget_content_width', self::DEFAULT_WIDTH, $args, $instance );
+
+ // Register a filter to modify the tiled_gallery_content_width, so Jetpack_Tiled_Gallery
+ // can appropriately size the tiles.
+ add_filter( 'tiled_gallery_content_width', array( $this, 'tiled_gallery_content_width' ) );
+
+ if ( method_exists( $this, $method ) ) {
+ echo $this->$method( $args, $instance );
+ }
+
+ // Remove the stored $_instance_width, as it is no longer needed
+ $this->_instance_width = null;
+
+ // Remove the filter, so any Jetpack_Tiled_Gallery in a post is not affected
+ remove_filter( 'tiled_gallery_content_width', array( $this, 'tiled_gallery_content_width' ) );
+
+ echo "\n" . '</div>'; // .widget-gallery-$type
+
+ echo "\n" . $after_widget;
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'gallery' );
+ }
+
+ /**
+ * Fetch the images attached to the gallery Widget
+ *
+ * @param array $instance The Widget instance for which you'd like attachments
+ * @return array Array of attachment objects for the Widget in $instance
+ */
+ public function get_attachments( $instance ) {
+ $ids = explode( ',', $instance['ids'] );
+
+ if ( isset( $instance['random'] ) && 'on' == $instance['random'] ) {
+ shuffle( $ids );
+ }
+
+ $attachments_query = new WP_Query(
+ array(
+ 'post__in' => $ids,
+ 'post_status' => 'inherit',
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'image',
+ 'posts_per_page' => -1,
+ 'orderby' => 'post__in',
+ )
+ );
+
+ $attachments = $attachments_query->get_posts();
+
+ wp_reset_postdata();
+
+ return $attachments;
+ }
+
+ /**
+ * Generate HTML for a rectangular, tiled Widget
+ *
+ * @param array $args Display arguments including before_title, after_title, before_widget, and after_widget.
+ * @param array $instance The Widget instance to generate HTML for
+ * @return string String of HTML representing a rectangular gallery
+ */
+ public function rectangular_widget( $args, $instance ) {
+ if ( ! class_exists( 'Jetpack_Tiled_Gallery' )
+ && ! class_exists( 'Jetpack_Tiled_Gallery_Layout_Rectangular' ) ) {
+ return;
+ }
+
+ Jetpack_Tiled_Gallery::default_scripts_and_styles();
+
+ $layout = new Jetpack_Tiled_Gallery_Layout_Rectangular( $instance['attachments'], $instance['link'], false, 3 );
+ return $layout->HTML();
+ }
+
+ /**
+ * Generate HTML for a square (grid style) Widget
+ *
+ * @param array $args Display arguments including before_title, after_title, before_widget, and after_widget.
+ * @param array $instance The Widget instance to generate HTML for
+ * @return string String of HTML representing a square gallery
+ */
+ public function square_widget( $args, $instance ) {
+ if ( ! class_exists( 'Jetpack_Tiled_Gallery' )
+ && ! class_exists( 'Jetpack_Tiled_Gallery_Layout_Square' ) ) {
+ return;
+ }
+
+ Jetpack_Tiled_Gallery::default_scripts_and_styles();
+
+ $layout = new Jetpack_Tiled_Gallery_Layout_Square( $instance['attachments'], $instance['link'], false, 3 );
+ return $layout->HTML();
+ }
+
+ /**
+ * Generate HTML for a circular (grid style) Widget
+ *
+ * @param array $args Display arguments including before_title, after_title, before_widget, and after_widget.
+ * @param array $instance The Widget instance to generate HTML for
+ * @return string String of HTML representing a circular gallery
+ */
+ public function circle_widget( $args, $instance ) {
+ if ( ! class_exists( 'Jetpack_Tiled_Gallery' )
+ && ! class_exists( 'Jetpack_Tiled_Gallery_Layout_Circle' ) ) {
+ return;
+ }
+
+ Jetpack_Tiled_Gallery::default_scripts_and_styles();
+
+ $layout = new Jetpack_Tiled_Gallery_Layout_Circle( $instance['attachments'], $instance['link'], false, 3 );
+ return $layout->HTML();
+ }
+
+ /**
+ * Generate HTML for a slideshow Widget
+ *
+ * @param array $args Display arguments including before_title, after_title, before_widget, and after_widget.
+ * @param array $instance The Widget instance to generate HTML for
+ * @return string String of HTML representing a slideshow gallery
+ */
+ public function slideshow_widget( $args, $instance ) {
+ global $content_width;
+
+ require_once plugin_dir_path( realpath( dirname( __FILE__ ) . '/../shortcodes/slideshow.php' ) ) . 'slideshow.php';
+
+ if ( ! class_exists( 'Jetpack_Slideshow_Shortcode' ) ) {
+ return;
+ }
+
+ if ( count( $instance['attachments'] ) < 1 ) {
+ return;
+ }
+
+ $slideshow = new Jetpack_Slideshow_Shortcode();
+
+ $slideshow->enqueue_scripts();
+
+ $gallery_instance = 'widget-' . $args['widget_id'];
+
+ $gallery = array();
+
+ foreach ( $instance['attachments'] as $attachment ) {
+ $attachment_image_src = wp_get_attachment_image_src( $attachment->ID, 'full' );
+ $attachment_image_src = jetpack_photon_url( $attachment_image_src[0], array( 'w' => $this->_instance_width ) ); // [url, width, height]
+
+ $caption = wptexturize( strip_tags( $attachment->post_excerpt ) );
+
+ $gallery[] = (object) array(
+ 'src' => (string) esc_url_raw( $attachment_image_src ),
+ 'id' => (string) $attachment->ID,
+ 'caption' => (string) $caption,
+ );
+ }
+
+ $max_width = intval( get_option( 'large_size_w' ) );
+ $max_height = 175;
+
+ if ( intval( $content_width ) > 0 ) {
+ $max_width = min( intval( $content_width ), $max_width );
+ }
+
+ $color = Jetpack_Options::get_option( 'slideshow_background_color', 'black' );
+ $autostart = isset( $attr['autostart'] ) ? $attr['autostart'] : true;
+
+ $js_attr = array(
+ 'gallery' => $gallery,
+ 'selector' => $gallery_instance,
+ 'width' => $max_width,
+ 'height' => $max_height,
+ 'trans' => 'fade',
+ 'color' => $color,
+ 'autostart' => $autostart,
+ );
+
+ $html = $slideshow->slideshow_js( $js_attr );
+
+ return $html;
+ }
+
+ /**
+ * tiled_gallery_content_width filter
+ *
+ * Used to adjust the content width of Jetpack_Tiled_Gallery's in sidebars
+ *
+ * $this->_instance_width is filtered in widget() and this filter is added then removed in widget()
+ *
+ * @param int $width int The original width value
+ * @return int The filtered width
+ */
+ public function tiled_gallery_content_width( $width ) {
+ return $this->_instance_width;
+ }
+
+ public function form( $instance ) {
+ $defaults = $this->defaults();
+ $allowed_values = $this->allowed_values();
+
+ $instance = wp_parse_args( (array) $instance, $defaults );
+
+ include dirname( __FILE__ ) . '/gallery/templates/form.php';
+ }
+
+ public function update( $new_instance, $old_instance ) {
+ $instance = $this->sanitize( $new_instance );
+
+ return $instance;
+ }
+
+ /**
+ * Sanitize the $instance's values to the set of allowed values. If a value is not acceptable,
+ * it is set to its default.
+ *
+ * Helps keep things nice and secure by whitelisting only allowed values
+ *
+ * @param array $instance The Widget instance to sanitize values for
+ * @return array $instance The Widget instance with values sanitized
+ */
+ public function sanitize( $instance ) {
+ $allowed_values = $this->allowed_values();
+ $defaults = $this->defaults();
+
+ foreach ( $instance as $key => $value ) {
+ $value = trim( $value );
+
+ if ( isset( $allowed_values[ $key ] ) && $allowed_values[ $key ] && ! array_key_exists( $value, $allowed_values[ $key ] ) ) {
+ $instance[ $key ] = $defaults[ $key ];
+ } else {
+ $instance[ $key ] = sanitize_text_field( $value );
+ }
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Return a multi-dimensional array of allowed values (and their labels) for all widget form
+ * elements
+ *
+ * To allow all values on an input, omit it from the returned array
+ *
+ * @return array Array of allowed values for each option
+ */
+ public function allowed_values() {
+ $max_columns = 5;
+
+ // Create an associative array of allowed column values. This just automates the generation of
+ // column <option>s, from 1 to $max_columns
+ $allowed_columns = array_combine( range( 1, $max_columns ), range( 1, $max_columns ) );
+
+ return array(
+ 'type' => array(
+ 'rectangular' => __( 'Tiles', 'jetpack' ),
+ 'square' => __( 'Square Tiles', 'jetpack' ),
+ 'circle' => __( 'Circles', 'jetpack' ),
+ 'slideshow' => __( 'Slideshow', 'jetpack' ),
+ ),
+ 'columns' => $allowed_columns,
+ 'link' => array(
+ 'carousel' => __( 'Carousel', 'jetpack' ),
+ 'post' => __( 'Attachment Page', 'jetpack' ),
+ 'file' => __( 'Media File', 'jetpack' ),
+ ),
+ );
+ }
+
+ /**
+ * Return an associative array of default values
+ *
+ * These values are used in new widgets as well as when sanitizing input. If a given value is not allowed,
+ * as defined in allowed_values(), that input is set to the default value defined here.
+ *
+ * @return array Array of default values for the Widget's options
+ */
+ public function defaults() {
+ return array(
+ 'title' => '',
+ 'type' => 'rectangular',
+ 'ids' => '',
+ 'columns' => 3,
+ 'link' => 'carousel',
+ );
+ }
+
+ public function enqueue_frontend_scripts() {
+ wp_register_script(
+ 'gallery-widget',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/widgets/gallery/js/gallery.min.js',
+ 'modules/widgets/gallery/js/gallery.js'
+ )
+ );
+
+ wp_enqueue_script( 'gallery-widget' );
+ }
+
+ public function enqueue_admin_scripts() {
+ global $pagenow;
+
+ if ( 'widgets.php' == $pagenow || 'customize.php' == $pagenow ) {
+ wp_enqueue_media();
+
+ wp_enqueue_script(
+ 'gallery-widget-admin',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/widgets/gallery/js/admin.min.js',
+ 'modules/widgets/gallery/js/admin.js'
+ ),
+ array(
+ 'media-models',
+ 'media-views',
+ ),
+ '20150501'
+ );
+
+ $js_settings = array(
+ 'thumbSize' => self::THUMB_SIZE,
+ );
+
+ wp_localize_script( 'gallery-widget-admin', '_wpGalleryWidgetAdminSettings', $js_settings );
+ wp_enqueue_style( 'gallery-widget-admin', plugins_url( '/gallery/css/admin.css', __FILE__ ) );
+ wp_style_add_data( 'gallery-widget-admin', 'rtl', 'replace' );
+ }
+ }
+}
+
+add_action( 'widgets_init', 'jetpack_gallery_widget_init' );
+
+function jetpack_gallery_widget_init() {
+ /**
+ * Allow the Gallery Widget to be enabled even when Core supports the Media Gallery Widget
+ *
+ * @module widgets
+ *
+ * @since 5.5.0
+ *
+ * @param bool false Whether to force-enable the gallery widget
+ */
+ if (
+ ! apply_filters( 'jetpack_force_enable_gallery_widget', false )
+ && class_exists( 'WP_Widget_Media_Gallery' )
+ && Jetpack_Options::get_option( 'gallery_widget_migration' )
+ ) {
+ return;
+ }
+ if ( ! method_exists( 'Jetpack', 'is_module_active' ) || Jetpack::is_module_active( 'tiled-gallery' ) ) {
+ register_widget( 'Jetpack_Gallery_Widget' );
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/gallery/css/admin-rtl.css b/plugins/jetpack/modules/widgets/gallery/css/admin-rtl.css
new file mode 100644
index 00000000..afd9550d
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gallery/css/admin-rtl.css
@@ -0,0 +1,12 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.gallery-widget-thumbs-wrapper {
+ margin: -5px 0 0.3em 0;
+}
+
+.gallery-widget-thumbs img {
+ border: 1px solid #ccc;
+ padding: 2px;
+ background-color: #fff;
+ margin: 0 0 5px 5px;
+ float: right;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets/gallery/css/admin-rtl.min.css b/plugins/jetpack/modules/widgets/gallery/css/admin-rtl.min.css
new file mode 100644
index 00000000..de937320
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gallery/css/admin-rtl.min.css
@@ -0,0 +1 @@
+.gallery-widget-thumbs-wrapper{margin:-5px 0 .3em 0}.gallery-widget-thumbs img{border:1px solid #ccc;padding:2px;background-color:#fff;margin:0 0 5px 5px;float:right} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets/gallery/css/admin.css b/plugins/jetpack/modules/widgets/gallery/css/admin.css
new file mode 100644
index 00000000..c3d96d80
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gallery/css/admin.css
@@ -0,0 +1,11 @@
+.gallery-widget-thumbs-wrapper {
+ margin: -5px 0 0.3em 0;
+}
+
+.gallery-widget-thumbs img {
+ border: 1px solid #ccc;
+ padding: 2px;
+ background-color: #fff;
+ margin: 0 5px 5px 0;
+ float: left;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets/gallery/css/admin.min.css b/plugins/jetpack/modules/widgets/gallery/css/admin.min.css
new file mode 100644
index 00000000..743791f9
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gallery/css/admin.min.css
@@ -0,0 +1,2 @@
+/* Do not modify this file directly. It is concatenated from individual module CSS files. */
+.gallery-widget-thumbs-wrapper{margin:-5px 0 .3em 0}.gallery-widget-thumbs img{border:1px solid #ccc;padding:2px;background-color:#fff;margin:0 5px 5px 0;float:left} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets/gallery/css/rtl/admin-rtl.css b/plugins/jetpack/modules/widgets/gallery/css/rtl/admin-rtl.css
new file mode 100644
index 00000000..e0e4b615
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gallery/css/rtl/admin-rtl.css
@@ -0,0 +1,13 @@
+/* This file was automatically generated on Mar 22 2013 21:33:14 */
+
+.gallery-widget-thumbs-wrapper {
+ margin: -5px 0 0.3em 0;
+}
+
+.gallery-widget-thumbs img {
+ border: 1px solid #ccc;
+ padding: 2px;
+ background-color: #fff;
+ margin: 0 0 5px 5px;
+ float: right;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets/gallery/js/admin.js b/plugins/jetpack/modules/widgets/gallery/js/admin.js
new file mode 100644
index 00000000..71025a9b
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gallery/js/admin.js
@@ -0,0 +1,236 @@
+/* jshint onevar: false, multistr: true */
+/* global _wpMediaViewsL10n, _wpGalleryWidgetAdminSettings */
+
+( function( $ ) {
+ var $ids;
+ var $thumbs;
+
+ $( function() {
+ $( document.body ).on( 'click', '.gallery-widget-choose-images', function( event ) {
+ event.preventDefault();
+
+ var widget_form = $( this ).closest( 'form, .form' );
+
+ $ids = widget_form.find( '.gallery-widget-ids' );
+ $thumbs = widget_form.find( '.gallery-widget-thumbs' );
+
+ var idsString = $ids.val();
+
+ var attachments = getAttachments( idsString );
+
+ var selection = null;
+ var editing = false;
+
+ if ( attachments ) {
+ selection = getSelection( attachments );
+
+ editing = true;
+ }
+
+ var options = {
+ state: 'gallery-edit',
+ title: wp.media.view.l10n.addMedia,
+ multiple: true,
+ editing: editing,
+ selection: selection,
+ };
+
+ var workflow = getWorkflow( options );
+
+ workflow.open();
+ } );
+
+ // Setup an onchange handler to toggle various options when changing style. The different style options
+ // require different form inputs to be presented in the widget; this event will keep the UI in sync
+ // with the selected style
+ $( '.widget-inside' ).on( 'change', '.gallery-widget-style', setupStyleOptions );
+
+ // Setup the Link To options for all forms currently on the page. Does the same as the onChange handler, but
+ // is called once to display the correct form inputs for each widget on the page
+ setupStyleOptions();
+ } );
+
+ var media = wp.media,
+ l10n;
+
+ // Link any localized strings.
+ l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
+
+ /**
+ * wp.media.view.MediaFrame.GalleryWidget
+ *
+ * This behavior can be very nearly had by setting the workflow's state to 'gallery-edit', but
+ * we cannot use the custom WidgetGalleryEdit controller with it (must overide createStates(),
+ * which is necessary to disable the sidebar gallery settings in the media browser)
+ */
+ media.view.MediaFrame.GalleryWidget = media.view.MediaFrame.Post.extend( {
+ createStates: function() {
+ var options = this.options;
+
+ // `CollectionEdit` and `CollectionAdd` were only introduced in r27214-core,
+ // so they may not be available yet.
+ if ( 'CollectionEdit' in media.controller ) {
+ this.states.add( [
+ new media.controller.CollectionEdit( {
+ type: 'image',
+ collectionType: 'gallery',
+ title: l10n.editGalleryTitle,
+ SettingsView: media.view.Settings.Gallery,
+ library: options.selection,
+ editing: options.editing,
+ menu: 'gallery',
+ } ),
+ new media.controller.CollectionAdd( {
+ type: 'image',
+ collectionType: 'gallery',
+ title: l10n.addToGalleryTitle,
+ } ),
+ ] );
+ } else {
+ // If `CollectionEdit` is not available, then use the old approach.
+
+ if ( ! ( 'WidgetGalleryEdit' in media.controller ) ) {
+ // Remove the gallery settings sidebar when editing widgets.
+ media.controller.WidgetGalleryEdit = media.controller.GalleryEdit.extend( {
+ gallerySettings: function(/*browser*/) {
+ return;
+ },
+ } );
+ }
+
+ this.states.add( [
+ new media.controller.WidgetGalleryEdit( {
+ library: options.selection,
+ editing: options.editing,
+ menu: 'gallery',
+ } ),
+ new media.controller.GalleryAdd( {} ),
+ ] );
+ }
+ },
+ } );
+
+ function setupStyleOptions() {
+ $( '.widget-inside .gallery-widget-style' ).each( function(/*i*/) {
+ var style = $( this ).val();
+
+ var form = $( this ).parents( 'form' );
+
+ switch ( style ) {
+ case 'slideshow':
+ form.find( '.gallery-widget-link-wrapper' ).hide();
+ form.find( '.gallery-widget-columns-wrapper' ).hide();
+
+ break;
+
+ default:
+ form.find( '.gallery-widget-link-wrapper' ).show();
+ form.find( '.gallery-widget-columns-wrapper' ).show();
+ }
+ } );
+ }
+
+ /**
+ * Take a given Selection of attachments and a thumbs wrapper div (jQuery object)
+ * and fill it with thumbnails
+ */
+ function setupThumbs( selection, wrapper ) {
+ wrapper.empty();
+
+ var imageSize = _wpGalleryWidgetAdminSettings.thumbSize;
+
+ selection.each( function( model ) {
+ var sizedUrl = model.get( 'url' ) + '?w=' + imageSize + '&h=' + imageSize + '&crop=true';
+
+ var thumb = jQuery( '<img>', {
+ src: sizedUrl,
+ alt: model.get( 'title' ),
+ title: model.get( 'title' ),
+ width: imageSize,
+ height: imageSize,
+ class: 'thumb',
+ } );
+
+ wrapper.append( thumb );
+ } );
+ }
+
+ /**
+ * Take a csv string of ids (as stored in db) and fetch a full Attachments collection
+ */
+ function getAttachments( idsString ) {
+ if ( ! idsString ) {
+ return null;
+ }
+
+ // Found in /wp-includes/js/media-editor.js
+ var shortcode = wp.shortcode.next( 'gallery', '[gallery ids="' + idsString + '"]' );
+
+ // Ignore the rest of the match object, to give attachments() below what it expects
+ shortcode = shortcode.shortcode;
+
+ var attachments = wp.media.gallery.attachments( shortcode );
+
+ return attachments;
+ }
+
+ /**
+ * Take an Attachments collection and return a corresponding Selection model that can be
+ * passed to a MediaFrame to prepopulate the gallery picker
+ */
+ function getSelection( attachments ) {
+ var selection = new wp.media.model.Selection( attachments.models, {
+ props: attachments.props.toJSON(),
+ multiple: true,
+ } );
+
+ selection.gallery = attachments.gallery;
+
+ // Fetch the query's attachments, and then break ties from the
+ // query to allow for sorting.
+ selection.more().done( function() {
+ // Break ties with the query.
+ selection.props.set( { query: false } );
+ selection.unmirror();
+ selection.props.unset( 'orderby' );
+ } );
+
+ return selection;
+ }
+
+ /**
+ * Create a media 'workflow' (MediaFrame). This is the main entry point for the media picker
+ */
+ function getWorkflow( options ) {
+ var workflow = new wp.media.view.MediaFrame.GalleryWidget( options );
+
+ workflow.on(
+ 'update',
+ function( selection ) {
+ var state = workflow.state();
+
+ selection = selection || state.get( 'selection' );
+
+ if ( ! selection ) {
+ return;
+ }
+
+ // Map the Models down into a simple array of ids that can be easily imploded to a csv string
+ var ids = selection.map( function( model ) {
+ return model.get( 'id' );
+ } );
+
+ var id_string = ids.join( ',' );
+
+ $ids.val( id_string ).trigger( 'change' );
+
+ setupThumbs( selection, $thumbs );
+ },
+ this
+ );
+
+ workflow.setState( workflow.options.state );
+
+ return workflow;
+ }
+} )( jQuery );
diff --git a/plugins/jetpack/modules/widgets/gallery/js/gallery.js b/plugins/jetpack/modules/widgets/gallery/js/gallery.js
new file mode 100644
index 00000000..5b952bb4
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gallery/js/gallery.js
@@ -0,0 +1,10 @@
+( function( $ ) {
+ // Fixes a bug with carousels being triggered even when a widget's Link To option is not set to carousel.
+ // Happens when another gallery is loaded on the page, either in a post or separate widget
+ $( 'body' ).on( 'click', '.widget-gallery .no-carousel .tiled-gallery-item a', function( event ) {
+ // Have to trigger default, instead of carousel
+ event.stopPropagation();
+
+ return true;
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/widgets/gallery/templates/form.php b/plugins/jetpack/modules/widgets/gallery/templates/form.php
new file mode 100644
index 00000000..f24cf1c2
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gallery/templates/form.php
@@ -0,0 +1,89 @@
+<p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php esc_html_e( 'Title:', 'jetpack' ); ?>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>"
+ type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" />
+ </label>
+</p>
+
+<p>
+ <label>
+ <?php esc_html_e( 'Images:', 'jetpack' ); ?>
+ </label>
+</p>
+
+<div class="gallery-widget-thumbs-wrapper">
+ <div class="gallery-widget-thumbs">
+ <?php
+
+ // Add the thumbnails to the widget box
+ $attachments = $this->get_attachments( $instance );
+
+ foreach( $attachments as $attachment ){
+ $url = add_query_arg( array(
+ 'w' => self::THUMB_SIZE,
+ 'h' => self::THUMB_SIZE,
+ 'crop' => 'true'
+ ), wp_get_attachment_url( $attachment->ID ) );
+
+ ?>
+
+ <img src="<?php echo esc_url( $url ); ?>" title="<?php echo esc_attr( $attachment->post_title ); ?>" alt="<?php echo esc_attr( $attachment->post_title ); ?>"
+ width="<?php echo self::THUMB_SIZE; ?>" height="<?php echo self::THUMB_SIZE; ?>" class="thumb" />
+ <?php } ?>
+ </div>
+
+ <div style="clear: both;"></div>
+</div>
+
+<p>
+ <a class="button gallery-widget-choose-images"><span class="wp-media-buttons-icon"></span> <?php esc_html_e( 'Choose Images', 'jetpack' ); ?></a>
+</p>
+
+<p class="gallery-widget-link-wrapper">
+ <label for="<?php echo $this->get_field_id( 'link' ); ?>"><?php esc_html_e( 'Link To:', 'jetpack' ); ?></label>
+ <select name="<?php echo $this->get_field_name( 'link' ); ?>" id="<?php echo $this->get_field_id( 'link' ); ?>" class="widefat">
+ <?php foreach ( $allowed_values['link'] as $key => $label ) {
+ $selected = '';
+
+ if ( $instance['link'] == $key ) {
+ $selected = "selected='selected' ";
+ } ?>
+
+ <option value="<?php echo $key; ?>" <?php echo $selected; ?>><?php echo esc_html( $label, 'jetpack' ); ?></option>
+ <?php } ?>
+ </select>
+</p>
+
+<p>
+ <label for="<?php echo $this->get_field_id( 'random' ); ?>"><?php esc_html_e( 'Random Order:', 'jetpack' ); ?></label>
+ <?php $checked = '';
+
+ if ( isset( $instance['random'] ) && $instance['random'] )
+ $checked = 'checked="checked"';
+
+ ?>
+ <input name="<?php echo $this->get_field_name( 'random' ); ?>" id="<?php echo $this->get_field_id( 'random' ); ?>" type="checkbox" <?php echo $checked; ?>>
+</p>
+
+<p class="gallery-widget-style-wrapper">
+ <label for="<?php echo $this->get_field_id( 'type' ); ?>"><?php esc_html_e( 'Style:', 'jetpack' ); ?></label>
+ <select name="<?php echo $this->get_field_name( 'type' ); ?>" id="<?php echo $this->get_field_id( 'type' ); ?>" class="widefat gallery-widget-style">
+ <?php foreach ( $allowed_values['type'] as $key => $label ) {
+ $selected = '';
+
+ if ( $instance['type'] == $key ) {
+ $selected = "selected='selected' ";
+ } ?>
+
+ <option value="<?php echo $key; ?>" <?php echo $selected; ?>><?php echo esc_html( $label, 'jetpack' ); ?></option>
+ <?php } ?>
+ </select>
+</p>
+
+<?php
+
+
+?>
+
+<?php // Hidden input to hold the selected image ids as a csv list ?>
+<input type="hidden" class="gallery-widget-ids" name="<?php echo $this->get_field_name( 'ids' ); ?>" id="<?php echo $this->get_field_id( 'ids' ); ?>" value="<?php echo esc_attr( $instance['ids'] ); ?>" />
diff --git a/plugins/jetpack/modules/widgets/goodreads.php b/plugins/jetpack/modules/widgets/goodreads.php
new file mode 100644
index 00000000..4160c868
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/goodreads.php
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Register the widget for use in Appearance -> Widgets
+ */
+add_action( 'widgets_init', 'jetpack_goodreads_widget_init' );
+
+function jetpack_goodreads_widget_init() {
+ register_widget( 'WPCOM_Widget_Goodreads' );
+}
+
+/**
+ * Goodreads widget class
+ * Display a user's Goodreads shelf.
+ * Customize user_id, title, and shelf
+ *
+ */
+class WPCOM_Widget_Goodreads extends WP_Widget {
+
+ private $goodreads_widget_id = 0;
+
+ function __construct() {
+ parent::__construct(
+ 'wpcom-goodreads',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Goodreads', 'jetpack' ) ),
+ array(
+ 'classname' => 'widget_goodreads',
+ 'description' => __( 'Display your books from Goodreads', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+ // For user input sanitization and display
+ $this->shelves = array(
+ 'read' => _x( 'Read', 'past participle: books I have read', 'jetpack' ),
+ 'currently-reading' => __( 'Currently Reading', 'jetpack' ),
+ 'to-read' => _x( 'To Read', 'my list of books to read', 'jetpack' ),
+ );
+
+ if ( is_active_widget( '', '', 'wpcom-goodreads' ) || is_customize_preview() ) {
+ add_action( 'wp_print_styles', array( $this, 'enqueue_style' ) );
+ }
+ }
+
+ function enqueue_style() {
+ wp_enqueue_style( 'goodreads-widget', plugins_url( 'goodreads/css/goodreads.css', __FILE__ ) );
+ wp_style_add_data( 'goodreads-widget', 'rtl', 'replace' );
+ }
+
+ function widget( $args, $instance ) {
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'goodreads' );
+
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', isset( $instance['title'] ) ? $instance['title'] : '' );
+
+ if ( empty( $instance['user_id'] ) || 'invalid' === $instance['user_id'] ) {
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ echo $args['before_widget'];
+ echo '<p>' . sprintf(
+ __( 'You need to enter your numeric user ID for the <a href="%1$s">Goodreads Widget</a> to work correctly. <a href="%2$s" target="_blank">Full instructions</a>.', 'jetpack' ),
+ esc_url( admin_url( 'widgets.php' ) ),
+ 'https://support.wordpress.com/widgets/goodreads-widget/#goodreads-user-id'
+ ) . '</p>';
+ echo $args['after_widget'];
+ }
+ return;
+ }
+
+ if ( ! array_key_exists( $instance['shelf'], $this->shelves ) ) {
+ return;
+ }
+
+ $instance['user_id'] = absint( $instance['user_id'] );
+
+ // Set widget ID based on shelf.
+ $this->goodreads_widget_id = $instance['user_id'] . '_' . $instance['shelf'];
+
+ if ( empty( $title ) ) {
+ $title = esc_html__( 'Goodreads', 'jetpack' );
+ }
+
+ echo $args['before_widget'];
+ echo $args['before_title'] . $title . $args['after_title'];
+
+ $goodreads_url = 'https://www.goodreads.com/review/custom_widget/' . urlencode( $instance['user_id'] ) . '.' . urlencode( $instance['title'] ) . ':%20' . urlencode( $instance['shelf'] ) . '?cover_position=&cover_size=small&num_books=5&order=d&shelf=' . urlencode( $instance['shelf'] ) . '&sort=date_added&widget_bg_transparent=&widget_id=' . esc_attr( $this->goodreads_widget_id );
+
+ echo '<div class="gr_custom_widget" id="gr_custom_widget_' . esc_attr( $this->goodreads_widget_id ) . '"></div>' . "\n";
+ echo '<script src="' . esc_url( $goodreads_url ) . '"></script>' . "\n";
+
+ echo $args['after_widget'];
+ }
+
+ function goodreads_user_id_exists( $user_id ) {
+ $url = "https://www.goodreads.com/user/show/$user_id/";
+ $response = wp_remote_head(
+ $url, array(
+ 'httpversion' => '1.1',
+ 'timeout' => 3,
+ 'redirection' => 2,
+ )
+ );
+ if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ function update( $new_instance, $old_instance ) {
+ $instance = $old_instance;
+
+ $instance['user_id'] = trim( wp_kses( stripslashes( $new_instance['user_id'] ), array() ) );
+ if ( ! empty( $instance['user_id'] ) && ( ! isset( $old_instance['user_id'] ) || $instance['user_id'] !== $old_instance['user_id'] ) ) {
+ if ( ! $this->goodreads_user_id_exists( $instance['user_id'] ) ) {
+ $instance['user_id'] = 'invalid';
+ }
+ }
+ $instance['title'] = wp_kses( stripslashes( $new_instance['title'] ), array() );
+ $shelf = wp_kses( stripslashes( $new_instance['shelf'] ), array() );
+ if ( array_key_exists( $shelf, $this->shelves ) ) {
+ $instance['shelf'] = $shelf;
+ }
+
+ return $instance;
+ }
+
+ function form( $instance ) {
+ //Defaults
+ $instance = wp_parse_args(
+ (array) $instance, array(
+ 'user_id' => '',
+ 'title' => 'Goodreads',
+ 'shelf' => 'read',
+ )
+ );
+
+ echo '<p><label for="' . esc_attr( $this->get_field_id( 'title' ) ) . '">' . esc_html__( 'Title:', 'jetpack' ) . '
+ <input class="widefat" id="' . esc_attr( $this->get_field_id( 'title' ) ) . '" name="' . esc_attr( $this->get_field_name( 'title' ) ) . '" type="text" value="' . esc_attr( $instance['title'] ) . '" />
+ </label></p>
+ <p><label for="' . esc_attr( $this->get_field_id( 'user_id' ) ) . '">';
+ printf( __( 'Goodreads numeric user ID <a href="%s" target="_blank">(instructions)</a>:', 'jetpack' ), 'https://en.support.wordpress.com/widgets/goodreads-widget/#goodreads-user-id' );
+ if ( 'invalid' === $instance['user_id'] ) {
+ printf( '<br /><small class="error">%s</small>&nbsp;', __( 'Invalid User ID, please verify and re-enter your Goodreads numeric user ID.', 'jetpack' ) );
+ $instance['user_id'] = '';
+ }
+ echo '<input class="widefat" id="' . esc_attr( $this->get_field_id( 'user_id' ) ) . '" name="' . esc_attr( $this->get_field_name( 'user_id' ) ) . '" type="text" value="' . esc_attr( $instance['user_id'] ) . '" />
+ </label></p>
+ <p><label for="' . esc_attr( $this->get_field_id( 'shelf' ) ) . '">' . esc_html__( 'Shelf:', 'jetpack' ) . '
+ <select class="widefat" id="' . esc_attr( $this->get_field_id( 'shelf' ) ) . '" name="' . esc_attr( $this->get_field_name( 'shelf' ) ) . '" >';
+ foreach ( $this->shelves as $_shelf_value => $_shelf_display ) {
+ echo "\t<option value='" . esc_attr( $_shelf_value ) . "'" . selected( $_shelf_value, $instance['shelf'] ) . '>' . $_shelf_display . "</option>\n";
+ }
+ echo '</select>
+ </label></p>
+ ';
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/goodreads/css/goodreads.css b/plugins/jetpack/modules/widgets/goodreads/css/goodreads.css
new file mode 100644
index 00000000..d329cb7e
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/goodreads/css/goodreads.css
@@ -0,0 +1,48 @@
+div[class^="gr_custom_container"] {
+ /* customize your Goodreads widget container here*/
+ border: 1px solid gray;
+ border-radius:10px;
+ padding: 10px 5px 10px 5px;
+ background-color: #FFF;
+ color: #000;
+}
+
+div[class^="gr_custom_container"] a {
+ color: #000;
+}
+
+h2[class^="gr_custom_header"] {
+ /* customize your Goodreads header here*/
+ display: none;
+}
+div[class^="gr_custom_each_container"] {
+ /* customize each individual book container here */
+ width: 100%;
+ clear: both;
+ margin-bottom: 10px;
+ overflow: auto;
+ padding-bottom: 4px;
+ border-bottom: 1px solid #aaa;
+}
+div[class^="gr_custom_book_container"] {
+ /* customize your book covers here */
+ float: right;
+ overflow: hidden;
+ height: 60px;
+ margin-left: 4px;
+ width: 39px;
+}
+div[class^="gr_custom_author"] {
+ /* customize your author names here */
+ font-size: 10px;
+}
+div[class^="gr_custom_tags"] {
+ /* customize your tags here */
+ font-size: 10px;
+ color: gray;
+}
+div[class^="gr_custom_review"] {
+}
+div[class^="gr_custom_rating"] {
+ display: none;
+}
diff --git a/plugins/jetpack/modules/widgets/goodreads/css/rtl/goodreads-rtl.css b/plugins/jetpack/modules/widgets/goodreads/css/rtl/goodreads-rtl.css
new file mode 100644
index 00000000..ff600c49
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/goodreads/css/rtl/goodreads-rtl.css
@@ -0,0 +1,50 @@
+/* This file was automatically generated on Nov 19 2013 15:54:57 */
+
+div[class^="gr_custom_container"] {
+ /* customize your Goodreads widget container here*/
+ border: 1px solid gray;
+ border-radius:10px;
+ padding: 10px 5px 10px 5px;
+ background-color: #FFF;
+ color: #000;
+}
+
+div[class^="gr_custom_container"] a {
+ color: #000;
+}
+
+h2[class^="gr_custom_header"] {
+ /* customize your Goodreads header here*/
+ display: none;
+}
+div[class^="gr_custom_each_container"] {
+ /* customize each individual book container here */
+ width: 100%;
+ clear: both;
+ margin-bottom: 10px;
+ overflow: auto;
+ padding-bottom: 4px;
+ border-bottom: 1px solid #aaa;
+}
+div[class^="gr_custom_book_container"] {
+ /* customize your book covers here */
+ float: left;
+ overflow: hidden;
+ height: 60px;
+ margin-right: 4px;
+ width: 39px;
+}
+div[class^="gr_custom_author"] {
+ /* customize your author names here */
+ font-size: 10px;
+}
+div[class^="gr_custom_tags"] {
+ /* customize your tags here */
+ font-size: 10px;
+ color: gray;
+}
+div[class^="gr_custom_review"] {
+}
+div[class^="gr_custom_rating"] {
+ display: none;
+}
diff --git a/plugins/jetpack/modules/widgets/google-translate.php b/plugins/jetpack/modules/widgets/google-translate.php
new file mode 100644
index 00000000..241fb8ed
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/google-translate.php
@@ -0,0 +1,203 @@
+<?php
+/**
+ * Plugin Name: Google Translate Widget for WordPress.com
+ * Plugin URI: http://automattic.com
+ * Description: Add a widget for automatic translation
+ * Author: Artur Piszek
+ * Version: 0.1
+ * Author URI: http://automattic.com
+ * Text Domain: jetpack
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+class Jetpack_Google_Translate_Widget extends WP_Widget {
+ static $instance = null;
+
+ /**
+ * Default widget title.
+ *
+ * @var string $default_title
+ */
+ var $default_title;
+
+ /**
+ * Register widget with WordPress.
+ */
+ function __construct() {
+ parent::__construct(
+ 'google_translate_widget',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Google Translate', 'jetpack' ) ),
+ array(
+ 'description' => __( 'Provide your readers with the option to translate your site into their preferred language.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+
+ $this->default_title = esc_html__( 'Translate', 'jetpack' );
+ }
+
+ /**
+ * Enqueue frontend JS scripts.
+ */
+ public function enqueue_scripts() {
+ wp_register_script(
+ 'google-translate-init',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/widgets/google-translate/google-translate.min.js',
+ 'modules/widgets/google-translate/google-translate.js'
+ )
+ );
+ wp_register_script( 'google-translate', '//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit', array( 'google-translate-init' ) );
+ // Admin bar is also displayed on top of the site which causes google translate bar to hide beneath.
+ // Overwrite position of body.admin-bar
+ // This is a hack to show google translate bar a bit lower.
+ $lowerTranslateBar = '
+ .admin-bar {
+ position: inherit !important;
+ top: auto !important;
+ }
+ .admin-bar .goog-te-banner-frame {
+ top: 32px !important
+ }
+ @media screen and (max-width: 782px) {
+ .admin-bar .goog-te-banner-frame {
+ top: 46px !important;
+ }
+ }
+ @media screen and (max-width: 480px) {
+ .admin-bar .goog-te-banner-frame {
+ position: absolute;
+ }
+ }
+ ';
+ wp_add_inline_style( 'admin-bar', $lowerTranslateBar );
+ wp_add_inline_style( 'wpcom-admin-bar', $lowerTranslateBar );
+ }
+
+ /**
+ * Display the Widget.
+ *
+ * @see WP_Widget::widget()
+ *
+ * @param array $args Display arguments.
+ * @param array $instance The settings for the particular instance of the widget.
+ */
+ public function widget( $args, $instance ) {
+ // We never should show more than 1 instance of this.
+ if ( null === self::$instance ) {
+ $instance = wp_parse_args(
+ $instance, array(
+ 'title' => $this->default_title,
+ )
+ );
+
+ /**
+ * Filter the layout of the Google Translate Widget.
+ *
+ * 3 different integers are accepted.
+ * 0 for the vertical layout.
+ * 1 for the horizontal layout.
+ * 2 for the dropdown only.
+ *
+ * @see https://translate.google.com/manager/website/
+ *
+ * @module widgets
+ *
+ * @since 5.5.0
+ *
+ * @param string $layout layout of the Google Translate Widget.
+ */
+ $button_layout = apply_filters( 'jetpack_google_translate_widget_layout', 0 );
+
+ if (
+ ! is_int( $button_layout )
+ || 0 > $button_layout
+ || 2 < $button_layout
+ ) {
+ $button_layout = 0;
+ }
+
+ wp_localize_script(
+ 'google-translate-init',
+ '_wp_google_translate_widget',
+ array(
+ 'lang' => get_locale(),
+ 'layout' => intval( $button_layout ),
+ )
+ );
+ wp_enqueue_script( 'google-translate-init' );
+ wp_enqueue_script( 'google-translate' );
+
+ $title = $instance['title'];
+
+ if ( ! isset( $title ) ) {
+ $title = $this->default_title;
+ }
+
+ /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
+ $title = apply_filters( 'widget_title', $title );
+
+ echo $args['before_widget'];
+ if ( ! empty( $title ) ) {
+ echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
+ }
+ echo '<div id="google_translate_element"></div>';
+ echo $args['after_widget'];
+ self::$instance = $instance;
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'google-translate' );
+ }
+ }
+
+ /**
+ * Widget form in the dashboard.
+ *
+ * @see WP_Widget::form()
+ *
+ * @param array $instance Previously saved values from database.
+ */
+ public function form( $instance ) {
+ $title = isset( $instance['title'] ) ? $instance['title'] : false;
+ if ( false === $title ) {
+ $title = $this->default_title;
+ }
+ ?>
+<p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
+</p>
+ <?php
+ }
+
+ /**
+ * Sanitize widget form values as they are saved.
+ *
+ * @see WP_Widget::update()
+ *
+ * @param array $new_instance Values just sent to be saved.
+ * @param array $old_instance Previously saved values from database.
+ *
+ * @return array $instance Updated safe values to be saved.
+ */
+ public function update( $new_instance, $old_instance ) {
+ $instance = array();
+ $instance['title'] = wp_kses( $new_instance['title'], array() );
+ if ( $instance['title'] === $this->default_title ) {
+ $instance['title'] = false; // Store as false in case of language change
+ }
+ return $instance;
+ }
+
+}
+
+/**
+ * Register the widget for use in Appearance -> Widgets.
+ */
+function jetpack_google_translate_widget_init() {
+ register_widget( 'Jetpack_Google_Translate_Widget' );
+}
+add_action( 'widgets_init', 'jetpack_google_translate_widget_init' );
diff --git a/plugins/jetpack/modules/widgets/google-translate/google-translate.js b/plugins/jetpack/modules/widgets/google-translate/google-translate.js
new file mode 100644
index 00000000..31a1fa0f
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/google-translate/google-translate.js
@@ -0,0 +1,32 @@
+/*global google:true*/
+/*global _wp_google_translate_widget:true*/
+/*exported googleTranslateElementInit*/
+function googleTranslateElementInit() {
+ var lang = 'en';
+ var langParam;
+ var langRegex = /[?&#]lang=([a-zA-Z\-_]+)/;
+ if (
+ typeof _wp_google_translate_widget === 'object' &&
+ typeof _wp_google_translate_widget.lang === 'string'
+ ) {
+ lang = _wp_google_translate_widget.lang;
+ }
+ langParam = window.location.href.match( langRegex );
+ if ( langParam ) {
+ window.location.href =
+ window.location.href.replace( langRegex, '' ).replace( /#googtrans\([a-zA-Z\-_|]+\)/, '' ) +
+ '#googtrans(' +
+ lang +
+ '|' +
+ langParam[ 1 ] +
+ ')';
+ }
+ new google.translate.TranslateElement(
+ {
+ pageLanguage: lang,
+ layout: _wp_google_translate_widget.layout,
+ autoDisplay: false,
+ },
+ 'google_translate_element'
+ );
+}
diff --git a/plugins/jetpack/modules/widgets/gravatar-profile.css b/plugins/jetpack/modules/widgets/gravatar-profile.css
new file mode 100644
index 00000000..5316f6a3
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gravatar-profile.css
@@ -0,0 +1,46 @@
+.widget-grofile {
+}
+ .widget-grofile h4 {
+ margin: 1em 0 .5em;
+ }
+ .widget-grofile ul.grofile-urls {
+ margin-left: 0;
+ overflow: hidden;
+ }
+ .widget-grofile ul.grofile-accounts li {
+ list-style: none;
+ display: inline;
+ }
+
+ .widget-grofile ul.grofile-accounts li::before {
+ content: "" !important; /* Kubrick :( */
+ }
+ .widget-grofile .grofile-accounts-logo {
+ background-image: url('https://secure.gravatar.com/images/grav-share-sprite.png');
+ background-repeat: no-repeat;
+ /*background-position: -16px -16px;*/
+ width: 16px; /* So we don't show the topmost logo */
+ height: 16px; /* So we don't show the topmost logo */
+ float: left;
+ margin-right: 8px;
+ margin-bottom: 8px;
+ }
+ .rtl .widget-grofile .grofile-accounts-logo {
+ margin-left: 8px;
+ margin-right: 0;
+ }
+
+ .grofile-thumbnail {
+ width: 500px;
+ max-width: 100%;
+ }
+ @media
+only screen and (-webkit-min-device-pixel-ratio: 1.5),
+only screen and (-o-min-device-pixel-ratio: 3/2),
+only screen and (min--moz-device-pixel-ratio: 1.5),
+only screen and (min-device-pixel-ratio: 1.5) {
+ .widget-grofile .grofile-accounts-logo {
+ background-image: url('https://secure.gravatar.com/images/grav-share-sprite-2x.png');
+ background-size: 16px 784px;
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/gravatar-profile.php b/plugins/jetpack/modules/widgets/gravatar-profile.php
new file mode 100644
index 00000000..f5171665
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/gravatar-profile.php
@@ -0,0 +1,435 @@
+<?php
+
+/**
+ * Register the widget for use in Appearance -> Widgets
+ */
+add_action( 'widgets_init', 'jetpack_gravatar_profile_widget_init' );
+
+function jetpack_gravatar_profile_widget_init() {
+ register_widget( 'Jetpack_Gravatar_Profile_Widget' );
+}
+
+/**
+ * Display a widgetized version of your Gravatar Profile
+ * http://blog.gravatar.com/2010/03/26/gravatar-profiles/
+ */
+class Jetpack_Gravatar_Profile_Widget extends WP_Widget {
+
+ function __construct() {
+ parent::__construct(
+ 'grofile',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Gravatar Profile', 'jetpack' ) ),
+ array(
+ 'classname' => 'widget-grofile grofile',
+ 'description' => __( 'Display a mini version of your Gravatar Profile', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+
+ if ( is_admin() ) {
+ add_action( 'admin_footer-widgets.php', array( $this, 'admin_script' ) );
+ }
+
+ if ( is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+ }
+ }
+
+ function widget( $args, $instance ) {
+ /**
+ * Fires when an item is displayed on the front end.
+ *
+ * Can be used to track stats about the number of displays for a specific item
+ *
+ * @module widgets, shortcodes
+ *
+ * @since 1.6.0
+ *
+ * @param string widget_view Item type (e.g. widget, or embed).
+ * @param string grofile Item description (e.g. grofile, goodreads).
+ */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'grofile' );
+
+ $instance = wp_parse_args(
+ $instance, array(
+ 'title' => '',
+ 'email' => '',
+ )
+ );
+
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', $instance['title'] );
+
+ if ( ! $instance['email'] ) {
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ echo $args['before_widget'];
+ if ( ! empty( $title ) ) {
+ echo $args['before_title'] . $title . $args['after_title'];
+ }
+ echo '<p>' . sprintf( __( 'You need to select what to show in this <a href="%s">Gravatar Profile widget</a>.', 'jetpack' ), admin_url( 'widgets.php' ) ) . '</p>';
+ echo $args['after_widget'];
+ }
+ return;
+ }
+
+ echo $args['before_widget'];
+ if ( ! empty( $title ) ) {
+ echo $args['before_title'] . $title . $args['after_title'];
+ }
+
+ $profile = $this->get_profile( $instance['email'] );
+
+ if ( ! empty( $profile ) ) {
+ $profile = wp_parse_args(
+ $profile, array(
+ 'thumbnailUrl' => '',
+ 'profileUrl' => '',
+ 'displayName' => '',
+ 'aboutMe' => '',
+ 'urls' => array(),
+ 'accounts' => array(),
+ )
+ );
+ $gravatar_url = add_query_arg( 's', 320, $profile['thumbnailUrl'] ); // the default grav returned by grofiles is super small
+
+ // Enqueue front end assets.
+ $this->enqueue_scripts();
+
+ ?>
+ <img src="<?php echo esc_url( $gravatar_url ); ?>" class="grofile-thumbnail no-grav" alt="<?php echo esc_attr( $profile['displayName'] ); ?>" />
+ <div class="grofile-meta">
+ <h4><a href="<?php echo esc_url( $profile['profileUrl'] ); ?>"><?php echo esc_html( $profile['displayName'] ); ?></a></h4>
+ <p><?php echo wp_kses_post( $profile['aboutMe'] ); ?></p>
+ </div>
+
+ <?php
+
+ if ( $instance['show_personal_links'] ) {
+ $this->display_personal_links( (array) $profile['urls'] );
+ }
+
+ if ( $instance['show_account_links'] ) {
+ $this->display_accounts( (array) $profile['accounts'] );
+ }
+
+ ?>
+
+ <p><a href="<?php echo esc_url( $profile['profileUrl'] ); ?>" class="grofile-full-link">
+ <?php
+ echo esc_html(
+ /**
+ * Filter the Gravatar Profile widget's profile link title.
+ *
+ * @module widgets
+ *
+ * @since 2.8.0
+ *
+ * @param string $str Profile link title.
+ */
+ apply_filters(
+ 'jetpack_gravatar_full_profile_title',
+ __( 'View Full Profile &rarr;', 'jetpack' )
+ )
+ );
+ ?>
+ </a></p>
+
+ <?php
+ } else {
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ echo '<p>' . esc_html__( 'Error loading profile', 'jetpack' ) . '</p>';
+ }
+ }
+
+ echo $args['after_widget'];
+ }
+
+ function display_personal_links( $personal_links = array() ) {
+ if ( empty( $personal_links ) ) {
+ return;
+ }
+ ?>
+
+ <h4>
+ <?php
+ echo esc_html(
+ apply_filters(
+ /**
+ * Filter the Gravatar Profile widget's "Personal Links" section title.
+ *
+ * @module widgets
+ *
+ * @since 2.8.0
+ *
+ * @param string $str "Personal Links" section title.
+ */
+ 'jetpack_gravatar_personal_links_title',
+ __( 'Personal Links', 'jetpack' )
+ )
+ );
+ ?>
+ </h4>
+ <ul class="grofile-urls grofile-links">
+
+ <?php foreach ( $personal_links as $personal_link ) : ?>
+ <li>
+ <a href="<?php echo esc_url( $personal_link['value'] ); ?>">
+ <?php
+ $link_title = ( ! empty( $personal_link['title'] ) ) ? $personal_link['title'] : $personal_link['value'];
+ echo esc_html( $link_title );
+ ?>
+ </a>
+ </li>
+ <?php endforeach; ?>
+ </ul>
+
+ <?php
+ }
+
+ function display_accounts( $accounts = array() ) {
+ if ( empty( $accounts ) ) {
+ return;
+ }
+ ?>
+
+ <h4>
+ <?php
+ echo esc_html(
+ /**
+ * Filter the Gravatar Profile widget's "Verified Services" section title.
+ *
+ * @module widgets
+ *
+ * @since 2.8.0
+ *
+ * @param string $str "Verified Services" section title.
+ */
+ apply_filters(
+ 'jetpack_gravatar_verified_services_title',
+ __( 'Verified Services', 'jetpack' )
+ )
+ );
+ ?>
+ </h4>
+ <ul class="grofile-urls grofile-accounts">
+
+ <?php
+ foreach ( $accounts as $account ) :
+ if ( $account['verified'] != 'true' ) {
+ continue;
+ }
+
+ $sanitized_service_name = $this->get_sanitized_service_name( $account['shortname'] );
+ ?>
+
+ <li>
+ <a href="<?php echo esc_url( $account['url'] ); ?>" title="<?php echo sprintf( _x( '%1$s on %2$s', '1: User Name, 2: Service Name (Facebook, Twitter, ...)', 'jetpack' ), esc_html( $account['display'] ), esc_html( $sanitized_service_name ) ); ?>">
+ <span class="grofile-accounts-logo grofile-accounts-<?php echo esc_attr( $account['shortname'] ); ?> accounts_<?php echo esc_attr( $account['shortname'] ); ?>"></span>
+ </a>
+ </li>
+
+ <?php endforeach; ?>
+ </ul>
+
+ <?php
+ }
+
+ /**
+ * Enqueue CSS and JavaScript.
+ *
+ * @since 4.0.0
+ */
+ function enqueue_scripts() {
+ wp_enqueue_style(
+ 'gravatar-profile-widget',
+ plugins_url( 'gravatar-profile.css', __FILE__ ),
+ array(),
+ '20120711'
+ );
+
+ wp_enqueue_style(
+ 'gravatar-card-services',
+ 'https://secure.gravatar.com/css/services.css',
+ array(),
+ defined( 'GROFILES__CACHE_BUSTER' ) ? GROFILES__CACHE_BUSTER : gmdate( 'YW' )
+ );
+ }
+
+ function form( $instance ) {
+ $title = isset( $instance['title'] ) ? $instance['title'] : '';
+ $email = isset( $instance['email'] ) ? $instance['email'] : '';
+ $email_user = isset( $instance['email_user'] ) ? $instance['email_user'] : get_current_user_id();
+ $show_personal_links = isset( $instance['show_personal_links'] ) ? (bool) $instance['show_personal_links'] : '';
+ $show_account_links = isset( $instance['show_account_links'] ) ? (bool) $instance['show_account_links'] : '';
+ $profile_url = 'https://gravatar.com/profile/edit';
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $profile_url = admin_url( 'profile.php' );
+
+ if ( isset( $_REQUEST['calypso'] ) ) {
+ $profile_url = 'https://wordpress.com/me';
+ }
+ }
+ ?>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>">
+ <?php esc_html_e( 'Title', 'jetpack' ); ?> <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'email_user' ); ?>">
+ <?php esc_html_e( 'Select a user or pick "custom" and enter a custom email address.', 'jetpack' ); ?>
+ <br />
+
+ <?php
+ wp_dropdown_users(
+ array(
+ 'show_option_none' => __( 'Custom', 'jetpack' ),
+ 'selected' => $email_user,
+ 'name' => $this->get_field_name( 'email_user' ),
+ 'id' => $this->get_field_id( 'email_user' ),
+ 'class' => 'gravatar-profile-user-select',
+ )
+ );
+ ?>
+ </label>
+ </p>
+
+ <p class="gprofile-email-container <?php echo empty( $email_user ) || $email_user == -1 ? '' : 'hidden'; ?>">
+ <label for="<?php echo $this->get_field_id( 'email' ); ?>"><?php esc_html_e( 'Custom Email Address', 'jetpack' ); ?>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'email' ); ?>" name="<?php echo $this->get_field_name( 'email' ); ?>" type="text" value="<?php echo esc_attr( $email ); ?>" />
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'show_personal_links' ); ?>">
+ <input type="checkbox" name="<?php echo $this->get_field_name( 'show_personal_links' ); ?>" id="<?php echo $this->get_field_id( 'show_personal_links' ); ?>" <?php checked( $show_personal_links ); ?> />
+ <?php esc_html_e( 'Show Personal Links', 'jetpack' ); ?>
+ <br />
+ <small><?php esc_html_e( 'Links to your websites, blogs, or any other sites that help describe who you are.', 'jetpack' ); ?></small>
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'show_account_links' ); ?>">
+ <input type="checkbox" name="<?php echo $this->get_field_name( 'show_account_links' ); ?>" id="<?php echo $this->get_field_id( 'show_account_links' ); ?>" <?php checked( $show_account_links ); ?> />
+ <?php esc_html_e( 'Show Account Links', 'jetpack' ); ?>
+ <br />
+ <small><?php esc_html_e( 'Links to services that you use across the web.', 'jetpack' ); ?></small>
+ </label>
+ </p>
+
+ <p><a href="<?php echo esc_url( $profile_url ); ?>" target="_blank" title="<?php esc_attr_e( 'Opens in new window', 'jetpack' ); ?>"><?php esc_html_e( 'Edit Your Profile', 'jetpack' ); ?></a> | <a href="https://gravatar.com" target="_blank" title="<?php esc_attr_e( 'Opens in new window', 'jetpack' ); ?>"><?php esc_html_e( "What's a Gravatar?", 'jetpack' ); ?></a></p>
+
+ <?php
+ }
+
+ function admin_script() {
+ ?>
+ <script>
+ jQuery( function( $ ) {
+ $( '.wrap' ).on( 'change', '.gravatar-profile-user-select', function() {
+ var $input = $(this).closest('.widget-inside').find('.gprofile-email-container');
+ if ( '-1' === this.value.toLowerCase() ) {
+ $input.show();
+ } else {
+ $input.hide();
+ }
+ });
+ } );
+ </script>
+ <?php
+ }
+
+ function update( $new_instance, $old_instance ) {
+
+ $instance = array();
+
+ $instance['title'] = isset( $new_instance['title'] ) ? wp_kses( $new_instance['title'], array() ) : '';
+ $instance['email'] = isset( $new_instance['email'] ) ? wp_kses( $new_instance['email'], array() ) : '';
+ $instance['email_user'] = isset( $new_instance['email_user'] ) ? intval( $new_instance['email_user'] ) : -1;
+ $instance['show_personal_links'] = isset( $new_instance['show_personal_links'] ) ? (bool) $new_instance['show_personal_links'] : false;
+ $instance['show_account_links'] = isset( $new_instance['show_account_links'] ) ? (bool) $new_instance['show_account_links'] : false;
+
+ if ( $instance['email_user'] > 0 ) {
+ $user = get_userdata( $instance['email_user'] );
+ $instance['email'] = $user->user_email;
+ }
+
+ $hashed_email = md5( strtolower( trim( $instance['email'] ) ) );
+ $cache_key = 'grofile-' . $hashed_email;
+ delete_transient( $cache_key );
+
+ return $instance;
+ }
+
+ private function get_profile( $email ) {
+ $hashed_email = md5( strtolower( trim( $email ) ) );
+ $cache_key = 'grofile-' . $hashed_email;
+
+ if ( ! $profile = get_transient( $cache_key ) ) {
+ $profile_url = sprintf(
+ 'https://secure.gravatar.com/%s.json',
+ $hashed_email
+ );
+
+ $expire = 300;
+ $response = wp_remote_get(
+ esc_url_raw( $profile_url ),
+ array( 'User-Agent' => 'WordPress.com Gravatar Profile Widget' )
+ );
+ $response_code = wp_remote_retrieve_response_code( $response );
+ if ( 200 == $response_code ) {
+ $profile = wp_remote_retrieve_body( $response );
+ $profile = json_decode( $profile, true );
+
+ if ( is_array( $profile ) && ! empty( $profile['entry'] ) && is_array( $profile['entry'] ) ) {
+ $expire = 900; // cache for 15 minutes
+ $profile = $profile['entry'][0];
+ } else {
+ // Something strange happened. Cache for 5 minutes.
+ $profile = array();
+ }
+ } else {
+ $expire = 900; // cache for 15 minutes
+ $profile = array();
+ }
+
+ set_transient( $cache_key, $profile, $expire );
+ }
+ return $profile;
+ }
+
+ private function get_sanitized_service_name( $shortname ) {
+ // Some services have stylized or mixed cap names *cough* WP *cough*
+ switch ( $shortname ) {
+ case 'friendfeed':
+ return 'FriendFeed';
+ case 'linkedin':
+ return 'LinkedIn';
+ case 'yahoo':
+ return 'Yahoo!';
+ case 'youtube':
+ return 'YouTube';
+ // phpcs:ignore WordPress.WP.CapitalPDangit
+ case 'wordpress':
+ return 'WordPress';
+ case 'tripit':
+ return 'TripIt';
+ case 'myspace':
+ return 'MySpace';
+ case 'foursquare':
+ return 'foursquare';
+ case 'google':
+ return 'Google+';
+ default:
+ // Others don't
+ $shortname = ucwords( $shortname );
+ }
+ return $shortname;
+ }
+}
+
+// END
diff --git a/plugins/jetpack/modules/widgets/image-widget.php b/plugins/jetpack/modules/widgets/image-widget.php
new file mode 100644
index 00000000..37a353c1
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/image-widget.php
@@ -0,0 +1,274 @@
+<?php
+/**
+ * Module Name: Image Widget
+ * Module Description: Easily add images to your theme's sidebar.
+ * Sort Order: 20
+ * First Introduced: 1.2
+ */
+
+/**
+* Register the widget for use in Appearance -> Widgets
+*/
+add_action( 'widgets_init', 'jetpack_image_widget_init', 11 );
+function jetpack_image_widget_init() {
+ if ( class_exists( 'WP_Widget_Media_Image' ) && Jetpack_Options::get_option( 'image_widget_migration' ) ) {
+ return;
+ }
+ register_widget( 'Jetpack_Image_Widget' );
+}
+
+class Jetpack_Image_Widget extends WP_Widget {
+ /**
+ * Register widget with WordPress.
+ */
+ public function __construct() {
+ parent::__construct(
+ 'image',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', esc_html__( 'Image', 'jetpack' ) ),
+ array(
+ 'classname' => 'widget_image',
+ 'description' => __( 'Display an image in your sidebar', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+
+ if ( is_active_widget( false, false, $this->id_base ) || is_active_widget( false, false, 'monster' ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_style' ) );
+ }
+ }
+
+ /**
+ * Loads file for front-end widget style.
+ *
+ * @uses wp_enqueue_style(), plugins_url()
+ */
+ public function enqueue_style() {
+ wp_enqueue_style( 'jetpack_image_widget', plugins_url( 'image-widget/style.css', __FILE__ ), array(), '20140808' );
+ }
+
+ /**
+ * Front-end display of widget.
+ *
+ * @see WP_Widget::widget()
+ *
+ * @param array $args Widget arguments.
+ * @param array $instance Saved values from database.
+ */
+ public function widget( $args, $instance ) {
+ echo $args['before_widget'];
+
+ $instance = wp_parse_args(
+ $instance, array(
+ 'title' => '',
+ 'img_url' => '',
+ )
+ );
+
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', $instance['title'] );
+
+ if ( $title ) {
+ echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
+ }
+
+ if ( '' != $instance['img_url'] ) {
+
+ $output = '<img src="' . esc_url( $instance['img_url'] ) . '" ';
+
+ if ( '' != $instance['alt_text'] ) {
+ $output .= 'alt="' . esc_attr( $instance['alt_text'] ) . '" ';
+ }
+ if ( '' != $instance['img_title'] ) {
+ $output .= 'title="' . esc_attr( $instance['img_title'] ) . '" ';
+ }
+ if ( '' == $instance['caption'] ) {
+ $output .= 'class="align' . esc_attr( $instance['align'] ) . '" ';
+ }
+ if ( '' != $instance['img_width'] ) {
+ $output .= 'width="' . esc_attr( $instance['img_width'] ) . '" ';
+ }
+ if ( '' != $instance['img_height'] ) {
+ $output .= 'height="' . esc_attr( $instance['img_height'] ) . '" ';
+ }
+ $output .= '/>';
+
+ if ( class_exists( 'Jetpack_Photon' ) && Jetpack::is_module_active( 'photon' ) ) {
+ $output = Jetpack_Photon::filter_the_content( $output );
+ }
+
+ if ( '' != $instance['link'] ) {
+ $target = ! empty( $instance['link_target_blank'] )
+ ? 'target="_blank"'
+ : '';
+ $output = '<a ' . $target . ' href="' . esc_url( $instance['link'] ) . '">' . $output . '</a>';
+ }
+ if ( '' != $instance['caption'] ) {
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $caption = apply_filters( 'widget_text', $instance['caption'] );
+ $img_width = ( ! empty( $instance['img_width'] ) ? 'style="width: ' . esc_attr( $instance['img_width'] ) . 'px"' : '' );
+ $output = '<figure ' . $img_width . ' class="wp-caption align' . esc_attr( $instance['align'] ) . '">
+ ' . $output . '
+ <figcaption class="wp-caption-text">' . $caption . '</figcaption>
+ </figure>'; // wp_kses_post caption on update
+ }
+ echo '<div class="jetpack-image-container">' . do_shortcode( $output ) . '</div>';
+ } else {
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ echo '<p>' . sprintf( __( 'Image missing or invalid URL. Please check the Image widget URL in your <a href="%s">widget settings</a>.', 'jetpack' ), admin_url( 'widgets.php' ) ) . '</p>';
+ }
+ }
+
+ echo "\n" . $args['after_widget'];
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'image' );
+ }
+
+ /**
+ * Sanitize widget form values as they are saved.
+ *
+ * @see WP_Widget::update()
+ *
+ * @param array $new_instance Values just sent to be saved.
+ * @param array $old_instance Previously saved values from database.
+ *
+ * @return array Updated safe values to be saved.
+ */
+ public function update( $new_instance, $old_instance ) {
+ $allowed_caption_html = array(
+ 'a' => array(
+ 'href' => array(),
+ 'title' => array(),
+ ),
+ 'b' => array(),
+ 'em' => array(),
+ 'i' => array(),
+ 'p' => array(),
+ 'strong' => array(),
+ );
+
+ $instance = $old_instance;
+
+ $instance['title'] = strip_tags( $new_instance['title'] );
+ $instance['img_url'] = esc_url( trim( $new_instance['img_url'] ) );
+ $instance['alt_text'] = strip_tags( $new_instance['alt_text'] );
+ $instance['img_title'] = strip_tags( $new_instance['img_title'] );
+ $instance['caption'] = wp_kses( stripslashes( $new_instance['caption'] ), $allowed_caption_html );
+ $instance['align'] = $new_instance['align'];
+ $instance['link'] = esc_url( trim( $new_instance['link'] ) );
+ $instance['link_target_blank'] = isset( $new_instance['link_target_blank'] ) ? (bool) $new_instance['link_target_blank'] : false;
+
+ $new_img_width = absint( $new_instance['img_width'] );
+ $new_img_height = absint( $new_instance['img_height'] );
+
+ if ( ! empty( $instance['img_url'] ) && '' == $new_img_width && '' == $new_img_height ) {
+ // Download the url to a local temp file and then process it with getimagesize so we can optimize browser layout
+ $tmp_file = download_url( $instance['img_url'], 10 );
+ if ( ! is_wp_error( $tmp_file ) ) {
+ $size = getimagesize( $tmp_file );
+
+ $width = $size[0];
+ $instance['img_width'] = absint( $width );
+
+ $height = $size[1];
+ $instance['img_height'] = absint( $height );
+
+ unlink( $tmp_file );
+ } else {
+ $instance['img_width'] = $new_img_width;
+ $instance['img_height'] = $new_img_height;
+ }
+ } else {
+ $instance['img_width'] = $new_img_width;
+ $instance['img_height'] = $new_img_height;
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Back end widget form.
+ *
+ * @see WP_Widget::form()
+ *
+ * @param array $instance Previously saved values from database.
+ */
+ public function form( $instance ) {
+ // Defaults
+ $instance = wp_parse_args(
+ (array) $instance, array(
+ 'title' => '',
+ 'img_url' => '',
+ 'alt_text' => '',
+ 'img_title' => '',
+ 'caption' => '',
+ 'align' => 'none',
+ 'img_width' => '',
+ 'img_height' => '',
+ 'link' => '',
+ 'link_target_blank' => false,
+ )
+ );
+
+ $title = esc_attr( $instance['title'] );
+ $img_url = esc_url( $instance['img_url'], null, 'display' );
+ $alt_text = esc_attr( $instance['alt_text'] );
+ $img_title = esc_attr( $instance['img_title'] );
+ $caption = esc_textarea( $instance['caption'] );
+ $align = esc_attr( $instance['align'] );
+ $img_width = esc_attr( $instance['img_width'] );
+ $img_height = esc_attr( $instance['img_height'] );
+ $link_target_blank = checked( $instance['link_target_blank'], true, false );
+
+ $link = esc_url( $instance['link'], null, 'display' );
+
+ echo '<p><label for="' . $this->get_field_id( 'title' ) . '">' . esc_html__( 'Widget title:', 'jetpack' ) . '
+ <input class="widefat" id="' . $this->get_field_id( 'title' ) . '" name="' . $this->get_field_name( 'title' ) . '" type="text" value="' . $title . '" />
+ </label></p>
+ <p><label for="' . $this->get_field_id( 'img_url' ) . '">' . esc_html__( 'Image URL:', 'jetpack' ) . '
+ <input class="widefat" id="' . $this->get_field_id( 'img_url' ) . '" name="' . $this->get_field_name( 'img_url' ) . '" type="text" value="' . $img_url . '" />
+ </label></p>
+ <p><label for="' . $this->get_field_id( 'alt_text' ) . '">' . esc_html__( 'Alternate text:', 'jetpack' ) . ' <a href="http://support.wordpress.com/widgets/image-widget/#image-widget-alt-text" target="_blank">( ? )</a>
+ <input class="widefat" id="' . $this->get_field_id( 'alt_text' ) . '" name="' . $this->get_field_name( 'alt_text' ) . '" type="text" value="' . $alt_text . '" />
+ </label></p>
+ <p><label for="' . $this->get_field_id( 'img_title' ) . '">' . esc_html__( 'Image title:', 'jetpack' ) . ' <a href="http://support.wordpress.com/widgets/image-widget/#image-widget-title" target="_blank">( ? )</a>
+ <input class="widefat" id="' . $this->get_field_id( 'img_title' ) . '" name="' . $this->get_field_name( 'img_title' ) . '" type="text" value="' . $img_title . '" />
+ </label></p>
+ <p><label for="' . $this->get_field_id( 'caption' ) . '">' . esc_html__( 'Caption:', 'jetpack' ) . ' <a href="http://support.wordpress.com/widgets/image-widget/#image-widget-caption" target="_blank">( ? )</a>
+ <textarea class="widefat" id="' . $this->get_field_id( 'caption' ) . '" name="' . $this->get_field_name( 'caption' ) . '" rows="2" cols="20">' . $caption . '</textarea>
+ </label></p>';
+
+ $alignments = array(
+ 'none' => __( 'None', 'jetpack' ),
+ 'left' => __( 'Left', 'jetpack' ),
+ 'center' => __( 'Center', 'jetpack' ),
+ 'right' => __( 'Right', 'jetpack' ),
+ );
+ echo '<p><label for="' . $this->get_field_id( 'align' ) . '">' . esc_html__( 'Image Alignment:', 'jetpack' ) . '
+ <select id="' . $this->get_field_id( 'align' ) . '" name="' . $this->get_field_name( 'align' ) . '">';
+ foreach ( $alignments as $alignment => $alignment_name ) {
+ echo '<option value="' . esc_attr( $alignment ) . '" ';
+ if ( $alignment == $align ) {
+ echo 'selected="selected" ';
+ }
+ echo '>' . esc_html( $alignment_name ) . "</option>\n";
+ }
+ echo '</select></label></p>';
+
+ echo '<p><label for="' . $this->get_field_id( 'img_width' ) . '">' . esc_html__( 'Width in pixels:', 'jetpack' ) . '
+ <input size="3" id="' . $this->get_field_id( 'img_width' ) . '" name="' . $this->get_field_name( 'img_width' ) . '" type="text" value="' . $img_width . '" />
+ </label>
+ <label for="' . $this->get_field_id( 'img_height' ) . '">' . esc_html__( 'Height in pixels:', 'jetpack' ) . '
+ <input size="3" id="' . $this->get_field_id( 'img_height' ) . '" name="' . $this->get_field_name( 'img_height' ) . '" type="text" value="' . $img_height . '" />
+ </label><br />
+ <small>' . esc_html__( 'If empty, we will attempt to determine the image size.', 'jetpack' ) . '</small></p>
+ <p><label for="' . $this->get_field_id( 'link' ) . '">' . esc_html__( 'Link URL (when the image is clicked):', 'jetpack' ) . '
+ <input class="widefat" id="' . $this->get_field_id( 'link' ) . '" name="' . $this->get_field_name( 'link' ) . '" type="text" value="' . $link . '" />
+ </label>
+ <label for="' . $this->get_field_id( 'link_target_blank' ) . '">
+ <input type="checkbox" name="' . $this->get_field_name( 'link_target_blank' ) . '" id="' . $this->get_field_id( 'link_target_blank' ) . '" value="1"' . $link_target_blank . '/>
+ ' . esc_html__( 'Open link in a new window/tab', 'jetpack' ) . '
+ </label></p>';
+ }
+} // Class Jetpack_Image_Widget
diff --git a/plugins/jetpack/modules/widgets/image-widget/style.css b/plugins/jetpack/modules/widgets/image-widget/style.css
new file mode 100644
index 00000000..ae650f2a
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/image-widget/style.css
@@ -0,0 +1,13 @@
+/*
+ * Image Widget styles for Jetpack
+ */
+
+/* Clear floats */
+.jetpack-image-container:after {
+ clear: both;
+}
+.jetpack-image-container:before,
+.jetpack-image-container:after {
+ display: table;
+ content: "";
+}
diff --git a/plugins/jetpack/modules/widgets/internet-defense-league.php b/plugins/jetpack/modules/widgets/internet-defense-league.php
new file mode 100644
index 00000000..9a69d1ab
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/internet-defense-league.php
@@ -0,0 +1,153 @@
+<?php
+
+class Jetpack_Internet_Defense_League_Widget extends WP_Widget {
+
+ public $defaults = array();
+
+ public $variant;
+ public $variants = array();
+
+ public $campaign;
+ public $campaigns = array();
+ public $no_current = true;
+
+ public $badge;
+ public $badges = array();
+
+ function __construct() {
+ parent::__construct(
+ 'internet_defense_league_widget',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', esc_html__( 'Internet Defense League', 'jetpack' ) ),
+ array(
+ 'description' => esc_html__( 'Show your support for the Internet Defense League.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+
+ // When enabling campaigns other than 'none' or empty, change $no_current to false above.
+ $this->campaigns = array(
+ '' => esc_html__( 'All current and future campaigns', 'jetpack' ),
+ 'none' => esc_html__( 'None, just display the badge please', 'jetpack' ),
+ );
+
+ $this->variants = array(
+ 'banner' => esc_html__( 'Banner at the top of my site', 'jetpack' ),
+ 'modal' => esc_html__( 'Modal (Overlay Box)', 'jetpack' ),
+ );
+
+ $this->badges = array(
+ 'shield_badge' => esc_html__( 'Shield Badge', 'jetpack' ),
+ 'super_badge' => esc_html__( 'Super Badge', 'jetpack' ),
+ 'side_bar_badge' => esc_html__( 'Red Cat Badge', 'jetpack' ),
+ );
+
+ if ( $this->no_current === false ) {
+ $this->badges['none'] = esc_html__( 'Don\'t display a badge (just the campaign)', 'jetpack' );
+ }
+
+ $this->defaults = array(
+ 'campaign' => key( $this->campaigns ),
+ 'variant' => key( $this->variants ),
+ 'badge' => key( $this->badges ),
+ );
+ }
+
+ public function widget( $args, $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults );
+
+ if ( 'none' != $instance['badge'] ) {
+ if ( ! isset( $this->badges[ $instance['badge'] ] ) ) {
+ $instance['badge'] = $this->defaults['badge'];
+ }
+ $badge_url = esc_url( 'https://internetdefenseleague.org/images/badges/final/' . $instance['badge'] . '.png' );
+ $photon_badge_url = jetpack_photon_url( $badge_url );
+ $alt_text = esc_html__( 'Member of The Internet Defense League', 'jetpack' );
+ echo $args['before_widget'];
+ echo '<p><a href="https://internetdefenseleague.org/"><img src="' . $photon_badge_url . '" alt="' . $alt_text . '" style="max-width: 100%; height: auto;" /></a></p>';
+ echo $args['after_widget'];
+ }
+
+ if ( 'none' != $instance['campaign'] ) {
+ $this->campaign = $instance['campaign'];
+ $this->variant = $instance['variant'];
+ add_action( 'wp_footer', array( $this, 'footer_script' ) );
+ }
+
+ /** This action is already documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'internet_defense_league' );
+ }
+
+ public function footer_script() {
+ if ( ! isset( $this->campaigns[ $this->campaign ] ) ) {
+ $this->campaign = $this->defaults['campaign'];
+ }
+
+ if ( ! isset( $this->variants[ $this->variant ] ) ) {
+ $this->variant = $this->defaults['variant'];
+ }
+ ?>
+ <script type="text/javascript">
+ window._idl = {};
+ _idl.campaign = "<?php echo esc_js( $this->campaign ); ?>";
+ _idl.variant = "<?php echo esc_js( $this->variant ); ?>";
+ (function() {
+ var idl = document.createElement('script');
+ idl.type = 'text/javascript';
+ idl.async = true;
+ idl.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'members.internetdefenseleague.org/include/?url=' + (_idl.url || '') + '&campaign=' + (_idl.campaign || '') + '&variant=' + (_idl.variant || 'banner');
+ document.getElementsByTagName('body')[0].appendChild(idl);
+ })();
+ </script>
+ <?php
+ }
+
+ public function form( $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults );
+
+ // Hide first two form fields if no current campaigns.
+ if ( false === $this->no_current ) {
+ echo '<p><label>';
+ echo esc_html__( 'Which Internet Defense League campaign do you want to participate in?', 'jetpack' ) . '<br />';
+ $this->select( 'campaign', $this->campaigns, $instance['campaign'] );
+ echo '</label></p>';
+
+ echo '<p><label>';
+ echo esc_html__( 'How do you want to promote the campaign?', 'jetpack' ) . '<br />';
+ $this->select( 'variant', $this->variants, $instance['variant'] );
+ echo '</label></p>';
+ }
+
+ echo '<p><label>';
+ echo esc_html__( 'Which badge would you like to display?', 'jetpack' ) . '<br />';
+ $this->select( 'badge', $this->badges, $instance['badge'] );
+ echo '</label></p>';
+
+ /* translators: %s is a name of an internet campaign called the "Internet Defense League" */
+ echo '<p>' . sprintf( _x( 'Learn more about the %s', 'the Internet Defense League', 'jetpack' ), '<a href="https://www.internetdefenseleague.org/">Internet Defense League</a>' ) . '</p>';
+ }
+
+ public function select( $field_name, $options, $default = null ) {
+ echo '<select class="widefat" name="' . $this->get_field_name( $field_name ) . '">';
+ foreach ( $options as $option_slug => $option_name ) {
+ echo '<option value="' . esc_attr( $option_slug ) . '"' . selected( $option_slug, $default, false ) . '>' . esc_html( $option_name ) . '</option>';
+ }
+ echo '</select>';
+ }
+
+ public function update( $new_instance, $old_instance ) {
+ $instance = array();
+
+ $instance['campaign'] = ( isset( $new_instance['campaign'] ) && isset( $this->campaigns[ $new_instance['campaign'] ] ) ) ? $new_instance['campaign'] : $this->defaults['campaign'];
+ $instance['variant'] = ( isset( $new_instance['variant'] ) && isset( $this->variants[ $new_instance['variant'] ] ) ) ? $new_instance['variant'] : $this->defaults['variant'];
+ $instance['badge'] = ( isset( $new_instance['badge'] ) && isset( $this->badges[ $new_instance['badge'] ] ) ) ? $new_instance['badge'] : $this->defaults['badge'];
+
+ return $instance;
+ }
+}
+
+function jetpack_internet_defense_league_init() {
+ register_widget( 'Jetpack_Internet_Defense_League_Widget' );
+}
+
+add_action( 'widgets_init', 'jetpack_internet_defense_league_init' );
diff --git a/plugins/jetpack/modules/widgets/mailchimp.php b/plugins/jetpack/modules/widgets/mailchimp.php
new file mode 100644
index 00000000..a2aff1ba
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/mailchimp.php
@@ -0,0 +1,103 @@
+<?php
+
+if ( ! class_exists( 'Jetpack_MailChimp_Subscriber_Popup_Widget' ) ) {
+
+ if ( ! class_exists( 'MailChimp_Subscriber_Popup' ) ) {
+ include_once JETPACK__PLUGIN_DIR . 'modules/shortcodes/mailchimp.php';
+ }
+
+ //register MailChimp Subscriber Popup widget
+ function jetpack_mailchimp_subscriber_popup_widget_init() {
+ register_widget( 'Jetpack_MailChimp_Subscriber_Popup_Widget' );
+ }
+
+ add_action( 'widgets_init', 'jetpack_mailchimp_subscriber_popup_widget_init' );
+
+ /**
+ * Add a MailChimp subscription form.
+ */
+ class Jetpack_MailChimp_Subscriber_Popup_Widget extends WP_Widget {
+
+ /**
+ * Constructor
+ */
+ function __construct() {
+ parent::__construct(
+ 'widget_mailchimp_subscriber_popup',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'MailChimp Subscriber Popup', 'jetpack' ) ),
+ array(
+ 'classname' => 'widget_mailchimp_subscriber_popup',
+ 'description' => __( 'Allows displaying a popup subscription form to visitors.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+ }
+
+ /**
+ * Outputs the HTML for this widget.
+ *
+ * @param array $args An array of standard parameters for widgets in this theme
+ * @param array $instance An array of settings for this widget instance
+ *
+ * @return void Echoes it's output
+ **/
+ function widget( $args, $instance ) {
+ $instance = wp_parse_args( $instance, array( 'code' => '' ) );
+
+ // Regular expresion that will match maichimp shortcode.
+ $regex = '(\[mailchimp_subscriber_popup[^\]]+\])';
+
+ // Check if the shortcode exists.
+ preg_match( $regex, $instance['code'], $matches );
+
+ // Process the shortcode only, if exists.
+ if ( ! empty( $matches[0] ) ) {
+ echo do_shortcode( $matches[0] );
+ }
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'mailchimp_subscriber_popup' );
+ }
+
+
+ /**
+ * Deals with the settings when they are saved by the admin.
+ *
+ * @param array $new_instance New configuration values
+ * @param array $old_instance Old configuration values
+ *
+ * @return array
+ */
+ function update( $new_instance, $old_instance ) {
+ $instance = array();
+ $instance['code'] = MailChimp_Subscriber_Popup::reversal( $new_instance['code'] );
+
+ return $instance;
+ }
+
+
+ /**
+ * Displays the form for this widget on the Widgets page of the WP Admin area.
+ *
+ * @param array $instance Instance configuration.
+ *
+ * @return void
+ */
+ function form( $instance ) {
+ $instance = wp_parse_args( $instance, array( 'code' => '' ) );
+ ?>
+
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'code' ) ); ?>">
+ <?php printf( __( 'Code: <a href="%s" target="_blank">( ? )</a>', 'jetpack' ), 'https://en.support.wordpress.com/mailchimp/' ); ?>
+ </label>
+ <textarea class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'code' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'code' ) ); ?>" rows="3"><?php echo esc_textarea( $instance['code'] ); ?></textarea>
+ </p>
+
+ <?php
+ }
+
+ }
+
+}
diff --git a/plugins/jetpack/modules/widgets/migrate-to-core/gallery-widget.php b/plugins/jetpack/modules/widgets/migrate-to-core/gallery-widget.php
new file mode 100644
index 00000000..8cf2900f
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/migrate-to-core/gallery-widget.php
@@ -0,0 +1,198 @@
+<?php
+/**
+ * Migration from Jetpack's Gallery Widget to WordPress' Core Gallery Widget.
+ *
+ * @since 5.5
+ *
+ * @package Jetpack
+ */
+/**
+ * Migrates all active instances of Jetpack's Gallery widget to Core's Media Gallery widget.
+ */
+function jetpack_migrate_gallery_widget() {
+ // Only trigger the migration from wp-admin and outside unit tests
+ if ( ! is_admin() || defined( 'PHPUNIT_JETPACK_TESTSUITE' ) ) {
+ return;
+ }
+
+ // Only migrate if the new widget is available and we haven't yet migrated
+ if ( ! class_exists( 'WP_Widget_Media_Gallery' ) || Jetpack_Options::get_option( 'gallery_widget_migration' ) ) {
+ return;
+ }
+
+ $old_widgets = get_option( 'widget_gallery', array() );
+ $media_gallery = get_option( 'widget_media_gallery', array() );
+ $sidebars_widgets = wp_get_sidebars_widgets();
+
+ // Array to store legacy widget ids in to unregister on success.
+ $widgets_to_unregister = array();
+
+ $old_widgets = array_filter( $old_widgets, 'jetpack_migrate_gallery_widget_is_importable' );
+ foreach ( $old_widgets as $id => $widget ) {
+ $new_id = $id;
+ // Try to get an unique id for the new type of widget.
+ // It may be the case that the user has already created a core Gallery Widget
+ // before the migration begins. (Maybe Jetpack was deactivated during core's upgrade).
+ for( $i = 0; $i < 10 && in_array( $new_id, array_keys( $media_gallery ) ); $i++, $new_id++ );
+
+ $widget_copy = jetpack_migrate_gallery_widget_upgrade_widget( $widget );
+
+ if ( null === $widget_copy ) {
+ jetpack_migrate_gallery_widget_bump_stats( 'gallery-widget-skipped' );
+ continue;
+ }
+
+ $media_gallery[ $new_id ] = $widget_copy;
+
+ $sidebars_widgets = jetpack_migrate_gallery_widget_update_sidebars( $sidebars_widgets, $id, $new_id );
+
+ $widgets_to_unregister[ $id ] = $new_id;
+ }
+
+ if ( update_option( 'widget_media_gallery', $media_gallery ) ) {
+
+ // Now un-register old widgets and register new.
+ foreach ( $widgets_to_unregister as $id => $new_id ) {
+ wp_unregister_sidebar_widget( "gallery-${id}" );
+
+ // register new widget.
+ $media_gallery_widget = new WP_Widget_Media_Gallery();
+ $media_gallery_widget->_set( $new_id );
+ $media_gallery_widget->_register_one( $new_id );
+ }
+
+ wp_set_sidebars_widgets( $sidebars_widgets );
+
+ // Log if we migrated all, or some for this site.
+ foreach ( $widgets_to_unregister as $w ) {
+ jetpack_migrate_gallery_widget_bump_stats( 'gallery-widget-migrated' );
+ }
+
+ // We need to refresh on widgets page for changes to take effect.
+ // The jetpack_refresh_on_widget_page function is already defined in migrate-to-core/image-widget.php
+ add_action( 'current_screen', 'jetpack_refresh_on_widget_page' );
+ }
+ Jetpack_Options::update_option( 'gallery_widget_migration', true );
+}
+
+function jetpack_migrate_gallery_widget_is_importable( $widget ) {
+ // Can be caused by instantiating but not populating a widget in the Customizer.
+ if ( empty( $widget ) ) {
+ return false;
+ }
+ // The array as stored in the option constains two keys and one
+ // is a string `_multiwidget` which does not represent a widget, so we skip it
+ if ( ! is_array( $widget ) ) {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Returns a transformed version of the Gallery Widget.
+ * Will return null if the widget is either empty, is not an array or has more keys than expected
+ *
+ * @param $widget One of the Jetpack Gallery widgets to be transformed into a new Core Media Gallery Widget
+ *
+ * @return array|null
+ */
+function jetpack_migrate_gallery_widget_upgrade_widget( $widget ) {
+ $whitelisted_keys = array(
+ 'ids' => '',
+ 'link' => '',
+ 'title' => '',
+ 'type' => '',
+ 'random' => '',
+ 'conditions' => '',
+ );
+
+ $default_data = array(
+ 'columns' => 3,
+ 'ids' => array(),
+ 'link_type' => '',
+ 'orderby_random' => false,
+ 'size' => 'thumbnail',
+ 'title' => '',
+ 'type' => '',
+ );
+
+ if ( ! jetpack_migrate_gallery_widget_is_importable( $widget ) ) {
+ return null;
+ }
+ // Ensure widget has no keys other than those expected.
+ // Not all widgets have conditions, so lets add it in.
+ $widget_copy = array_merge( array( 'conditions' => null ), $widget );
+ $non_whitelisted_keys = array_diff_key( $widget_copy, $whitelisted_keys );
+ if ( count( $non_whitelisted_keys ) > 0 ) {
+ jetpack_migrate_gallery_widget_bump_stats( 'extra-key' );
+
+ // Log the names of the keys not in our whitelist.
+ foreach ( $non_whitelisted_keys as $key => $value ) {
+ jetpack_migrate_gallery_widget_bump_stats( "extra-key-$key", "migration-extra-key" );
+ }
+ }
+
+ $widget_copy = array_merge( $default_data, $widget, array(
+ // ids in Jetpack's Gallery are a string of comma-separated values.
+ // Core's Media Gallery Widget stores ids in an array
+ 'ids' => explode( ',', $widget['ids'] ),
+ 'link_type' => $widget['link'],
+ 'orderby_random' => isset( $widget['random'] ) && $widget['random'] === 'on',
+ ) );
+
+ // Unsetting old widget fields
+ $widget_copy = array_diff_key( $widget_copy, array(
+ 'link' => false,
+ 'random' => false,
+ ) );
+
+ return $widget_copy;
+}
+
+/**
+ * Replaces the references to Jetpack Gallery Widget in the sidebars for references to the new version of the widget
+ *
+ * @param $sidebars_widgets The sidebar widgets array to update
+ * @param $id Old id of the widget (basically its index in the array )
+ * @param $new_id New id that will be using on the sidebar as a new widget
+ *
+ * @return mixed Updated sidebar widgets array
+ */
+function jetpack_migrate_gallery_widget_update_sidebars( $sidebars_widgets, $id, $new_id ) {
+ foreach ( $sidebars_widgets as $sidebar => $widgets ) {
+ if (
+ is_array( $widgets )
+ && false !== ( $key = array_search( "gallery-{$id}", $widgets, true ) )
+ ) {
+ $sidebars_widgets[ $sidebar ][ $key ] = "media_gallery-{$new_id}";
+ // Check if the inactive widgets sidebar exists
+ // Related: https://core.trac.wordpress.org/ticket/14893
+ if ( ! isset( $sidebars_widgets['wp_inactive_widgets'] ) || ! is_array( $sidebars_widgets['wp_inactive_widgets'] ) ) {
+ $sidebars_widgets['wp_inactive_widgets'] = array();
+ }
+ $sidebars_widgets['wp_inactive_widgets'][ $key ] = "gallery-{$id}";
+ }
+ }
+ return $sidebars_widgets;
+}
+
+/**
+ * Will bump stat in jetpack_gallery_widget_migration group.
+ *
+ * @param string $bin The bin to log into.
+ * @param string $group The group name. Defaults to "widget-migration".
+ */
+function jetpack_migrate_gallery_widget_bump_stats( $bin, $group = 'widget-migration' ) {
+ // If this is being run on .com bumps_stats_extra exists, but using the filter looks more elegant.
+ if ( function_exists( 'bump_stats_extras' ) ) {
+ $group = "jetpack-$group";
+ do_action( 'jetpack_bump_stats_extra', $group, $bin );
+ } else {
+ // $group is prepended with 'jetpack-'
+ $jetpack = Jetpack::init();
+ $jetpack->stat( $group, $bin ) ;
+ }
+
+}
+
+add_action( 'widgets_init', 'jetpack_migrate_gallery_widget' );
diff --git a/plugins/jetpack/modules/widgets/migrate-to-core/image-widget.php b/plugins/jetpack/modules/widgets/migrate-to-core/image-widget.php
new file mode 100644
index 00000000..70d2d12b
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/migrate-to-core/image-widget.php
@@ -0,0 +1,220 @@
+<?php
+/**
+ * Migration from Jetpack's Image Widget to WordPress' Core Image Widget.
+ *
+ * @since 4.9
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Migrates all active instances of Jetpack's image widget to Core's media image widget.
+ */
+function jetpack_migrate_image_widget() {
+ // Only trigger the migration from wp-admin
+ if ( ! is_admin() ) {
+ return;
+ }
+
+ // Only migrate if the new widget is available and we haven't yet migrated
+ if ( ! class_exists( 'WP_Widget_Media_Image' ) || Jetpack_Options::get_option( 'image_widget_migration' ) ) {
+ return;
+ }
+
+ $default_data = array(
+ 'attachment_id' => 0,
+ 'url' => '',
+ 'title' => '',
+ 'size' => 'custom',
+ 'width' => 0,
+ 'height' => 0,
+ 'align' => 'none',
+ 'caption' => '',
+ 'alt' => '',
+ 'link_type' => '',
+ 'link_url' => '',
+ 'image_classes' => '',
+ 'link_classes' => '',
+ 'link_rel' => '',
+ 'image_title' => '',
+ 'link_target_blank' => false,
+ 'conditions' => null,
+ );
+
+ $old_widgets = get_option( 'widget_image', array() );
+ $media_image = get_option( 'widget_media_image', array() );
+ $sidebars_widgets = wp_get_sidebars_widgets();
+
+ // Persist old and current widgets in backup table.
+ jetpack_store_migration_data( 'widget_image', maybe_serialize( $old_widgets ) );
+ if ( jetpack_get_migration_data( 'widget_image' ) !== $old_widgets ) {
+ return false;
+ }
+
+ jetpack_store_migration_data( 'sidebars_widgets', maybe_serialize( $sidebars_widgets ) );
+ if ( jetpack_get_migration_data( 'sidebars_widgets' ) !== $sidebars_widgets ) {
+ return false;
+ }
+
+ // Array to store legacy widget ids in to unregister on success.
+ $widgets_to_unregister = array();
+
+ foreach ( $old_widgets as $id => $widget ) {
+ if ( is_string( $id ) ) {
+ continue;
+ }
+
+ // Can be caused by instanciating but not populating a widget in the Customizer.
+ if ( empty( $widget ) ) {
+ continue;
+ }
+
+ // Ensure widget has no keys other than those expected.
+ // Not all widgets have conditions, so lets add it in.
+ $widget_copy = array_merge( array( 'conditions' => null ), $widget );
+ $non_whitelisted_keys = array_diff_key( $widget_copy, array(
+ 'title' => '',
+ 'img_url' => '',
+ 'alt_text' => '',
+ 'img_title' => '',
+ 'caption' => '',
+ 'align' => '',
+ 'img_width' => '',
+ 'img_height' => '',
+ 'link' => '',
+ 'link_target_blank' => '',
+ 'conditions' => '',
+ ) );
+
+ if ( count( $non_whitelisted_keys ) > 0 ) {
+ // skipping the widget in question
+ continue;
+ }
+
+ $media_image[ $id ] = array_merge( $default_data, $widget, array(
+ 'alt' => $widget['alt_text'],
+ 'height' => $widget['img_height'],
+ 'image_classes' => ! empty( $widget['align'] ) ? 'align' . $widget['align'] : '',
+ 'image_title' => $widget['img_title'],
+ 'link_url' => $widget['link'],
+ 'url' => $widget['img_url'],
+ 'width' => $widget['img_width'],
+ ) );
+
+ // Unsetting old widget fields
+ $media_image[ $id ] = array_diff_key( $media_image[ $id ], array(
+ 'align' => false,
+ 'alt_text' => false,
+ 'img_height' => false,
+ 'img_title' => false,
+ 'img_url' => false,
+ 'img_width' => false,
+ 'link' => false,
+ ) );
+
+ // Check if the image is in the media library.
+ $image_basename = basename( $widget['img_url'] );
+
+ if ( empty( $image_basename ) ) {
+ continue;
+ }
+
+ $attachment_ids = get_posts( array(
+ 'fields' => 'ids',
+ 'meta_query' => array(
+ array(
+ 'value' => basename( $image_basename ),
+ 'compare' => 'LIKE',
+ 'key' => '_wp_attachment_metadata',
+ ),
+ ),
+ 'post_status' => 'inherit',
+ 'post_type' => 'attachment',
+ 'suppress_filters' => false,
+ ) );
+
+ foreach ( $attachment_ids as $attachment_id ) {
+ $image_meta = wp_get_attachment_metadata( $attachment_id );
+
+ // Is it a full size image?
+ $image_path_pieces = explode( '/', $image_meta['file'] );
+ if ( $image_basename === array_pop( $image_path_pieces ) ) {
+ $media_image[ $id ]['attachment_id'] = $attachment_id;
+
+ // Set correct size if dimensions fit.
+ if (
+ $media_image[ $id ]['width'] == $image_meta['width'] ||
+ $media_image[ $id ]['height'] == $image_meta['height']
+ ) {
+ $media_image[ $id ]['size'] = 'full';
+ }
+ break;
+ }
+
+ // Is it a down-sized image?
+ foreach ( $image_meta['sizes'] as $size => $image ) {
+ if ( false !== array_search( $image_basename, $image ) ) {
+ $media_image[ $id ]['attachment_id'] = $attachment_id;
+
+ // Set correct size if dimensions fit.
+ if (
+ $media_image[ $id ]['width'] == $image['width'] ||
+ $media_image[ $id ]['height'] == $image['height']
+ ) {
+ $media_image[ $id ]['size'] = $size;
+ }
+ break 2;
+ }
+ }
+ }
+
+ if ( ! empty( $widget['link'] ) ) {
+ $media_image[ $id ]['link_type'] = $widget['link'] === $widget['img_url'] ? 'file' : 'custom';
+ }
+
+ foreach ( $sidebars_widgets as $sidebar => $widgets ) {
+ if (
+ is_array( $widgets )
+ && false !== ( $key = array_search( "image-{$id}", $widgets, true ) )
+ ) {
+ $sidebars_widgets[ $sidebar ][ $key ] = "media_image-{$id}";
+ }
+ }
+
+ $widgets_to_unregister[] = $id;
+ }
+
+ if ( update_option( 'widget_media_image', $media_image ) ) {
+ delete_option( 'widget_image' );
+
+ // Now un-register old widgets and register new.
+ foreach ( $widgets_to_unregister as $id ) {
+ wp_unregister_sidebar_widget( "image-${id}" );
+
+ // register new widget.
+ $media_image_widget = new WP_Widget_Media_Image();
+ $media_image_widget->_set( $id );
+ $media_image_widget->_register_one( $id );
+ }
+
+ wp_set_sidebars_widgets( $sidebars_widgets );
+
+ // We need to refresh on widgets page for changes to take effect.
+ add_action( 'current_screen', 'jetpack_refresh_on_widget_page' );
+ } else {
+ $widget_media_image = get_option( 'widget_media_image' );
+ if ( is_array( $widget_media_image ) ) {
+ delete_option( 'widget_image' );
+ }
+ }
+
+ Jetpack_Options::update_option( 'image_widget_migration', true );
+}
+add_action( 'widgets_init', 'jetpack_migrate_image_widget' );
+
+function jetpack_refresh_on_widget_page( $current ) {
+ if ( 'widgets' === $current->base ) {
+ wp_safe_redirect( admin_url( 'widgets.php' ) );
+ exit;
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/milestone.php b/plugins/jetpack/modules/widgets/milestone.php
new file mode 100644
index 00000000..0dd2140e
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/milestone.php
@@ -0,0 +1,5 @@
+<?php
+/**
+ * Register the milestone widget. This makes it easier to keep the /milestone/ dir content in sync with wpcom.
+ */
+include dirname( __FILE__ ) . '/milestone/milestone.php';
diff --git a/plugins/jetpack/modules/widgets/milestone/admin.js b/plugins/jetpack/modules/widgets/milestone/admin.js
new file mode 100644
index 00000000..1897f0b7
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/milestone/admin.js
@@ -0,0 +1,31 @@
+( function( $ ) {
+ // We could either be in wp-admin/widgets.php or the customizer.
+ var $container = $( '#customize-controls' );
+
+ if ( ! $container.length ) {
+ $container = $( '#wpbody' );
+ }
+
+ $container.on( 'change', '.milestone-type', function() {
+ var $messageWrapper = $( this )
+ .parent()
+ .find( '.milestone-message-wrapper' );
+
+ $( this )
+ .find( 'input[type="radio"]:checked' )
+ .val() === 'since'
+ ? $messageWrapper.hide()
+ : $messageWrapper.show();
+ } );
+
+ function triggerChange() {
+ $container.find( '.milestone-type' ).trigger( 'change' );
+ }
+
+ // Used when adding widget via customizer or saving settings.
+ $( document ).on( 'widget-added widget-updated', function() {
+ triggerChange();
+ } );
+
+ triggerChange();
+} )( jQuery );
diff --git a/plugins/jetpack/modules/widgets/milestone/milestone.js b/plugins/jetpack/modules/widgets/milestone/milestone.js
new file mode 100644
index 00000000..3780dec6
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/milestone/milestone.js
@@ -0,0 +1,49 @@
+/* global MilestoneConfig */
+
+var Milestone = ( function( $ ) {
+ var Milestone = function( args ) {
+ var $widget = $( '#' + args.id ),
+ id = args.id,
+ refresh = args.refresh * 1000;
+
+ this.timer = function() {
+ var instance = this;
+
+ $.ajax( {
+ url: MilestoneConfig.api_root + 'jetpack/v4/widgets/' + id,
+ success: function( result ) {
+ $widget.find( '.milestone-countdown' ).replaceWith( result.message );
+ refresh = result.refresh * 1000;
+
+ if ( ! refresh ) {
+ return;
+ }
+
+ setTimeout( function() {
+ instance.timer();
+ }, refresh );
+ },
+ } );
+ };
+
+ if ( refresh > 0 ) {
+ this.timer();
+ }
+ };
+ return function( args ) {
+ return new Milestone( args );
+ };
+} )( jQuery );
+
+( function() {
+ var i,
+ MilestoneInstances = {};
+
+ if ( typeof MilestoneConfig === 'undefined' ) {
+ return;
+ }
+
+ for ( i = 0; i < MilestoneConfig.instances.length; i++ ) {
+ MilestoneInstances[ i ] = new Milestone( MilestoneConfig.instances[ i ] );
+ }
+} )();
diff --git a/plugins/jetpack/modules/widgets/milestone/milestone.php b/plugins/jetpack/modules/widgets/milestone/milestone.php
new file mode 100644
index 00000000..8490a8ec
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/milestone/milestone.php
@@ -0,0 +1,683 @@
+<?php
+/*
+Plugin Name: Milestone
+Description: Countdown to a specific date.
+Version: 1.0
+Author: Automattic Inc.
+Author URI: http://automattic.com/
+License: GPLv2 or later
+*/
+
+function jetpack_register_widget_milestone() {
+ register_widget( 'Milestone_Widget' );
+}
+add_action( 'widgets_init', 'jetpack_register_widget_milestone' );
+
+class Milestone_Widget extends WP_Widget {
+ private static $dir = null;
+ private static $url = null;
+ private static $defaults = null;
+ private static $config_js = null;
+
+ /**
+ * Available time units sorted in descending order.
+ * @var Array
+ */
+ protected $available_units = array(
+ 'years',
+ 'months',
+ 'days',
+ 'hours',
+ 'minutes',
+ 'seconds'
+ );
+
+ function __construct() {
+ $widget = array(
+ 'classname' => 'milestone-widget',
+ 'description' => __( 'Display a countdown to a certain date.', 'jetpack' ),
+ );
+
+ parent::__construct(
+ 'Milestone_Widget',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Milestone', 'jetpack' ) ),
+ $widget
+ );
+
+ self::$dir = trailingslashit( dirname( __FILE__ ) );
+ self::$url = plugin_dir_url( __FILE__ );
+
+ add_action( 'wp_enqueue_scripts', array( __class__, 'enqueue_template' ) );
+ add_action( 'admin_enqueue_scripts', array( __class__, 'enqueue_admin' ) );
+ add_action( 'wp_footer', array( $this, 'localize_script' ) );
+
+ if ( is_active_widget( false, false, $this->id_base, true ) || is_active_widget( false, false, 'monster', true ) || is_customize_preview() ) {
+ add_action( 'wp_head', array( __class__, 'styles_template' ) );
+ }
+ }
+
+ public static function enqueue_admin( $hook_suffix ) {
+ if ( 'widgets.php' == $hook_suffix ) {
+ wp_enqueue_style( 'milestone-admin', self::$url . 'style-admin.css', array(), '20161215' );
+ wp_enqueue_script(
+ 'milestone-admin-js',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/widgets/milestone/admin.min.js',
+ 'modules/widgets/milestone/admin.js'
+ ),
+ array( 'jquery' ),
+ '20170915',
+ true
+ );
+ }
+ }
+
+ public static function enqueue_template() {
+ if ( Jetpack_AMP_Support::is_amp_request() ) {
+ return;
+ }
+
+ wp_enqueue_script(
+ 'milestone',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/widgets/milestone/milestone.min.js',
+ 'modules/widgets/milestone/milestone.js'
+ ),
+ array( 'jquery' ),
+ '20160520',
+ true
+ );
+ }
+
+ public static function styles_template() {
+ global $themecolors;
+ $colors = wp_parse_args( $themecolors, array(
+ 'bg' => 'ffffff',
+ 'border' => 'cccccc',
+ 'text' => '333333',
+ ) );
+?>
+<style>
+.milestone-widget {
+ margin-bottom: 1em;
+}
+.milestone-content {
+ line-height: 2;
+ margin-top: 5px;
+ max-width: 100%;
+ padding: 0;
+ text-align: center;
+}
+.milestone-header {
+ background-color: <?php echo self::sanitize_color_hex( $colors['text'] ); ?>;
+ color: <?php echo self::sanitize_color_hex( $colors['bg'] ); ?>;
+ line-height: 1.3;
+ margin: 0;
+ padding: .8em;
+}
+.milestone-header .event,
+.milestone-header .date {
+ display: block;
+}
+.milestone-header .event {
+ font-size: 120%;
+}
+.milestone-countdown .difference {
+ display: block;
+ font-size: 500%;
+ font-weight: bold;
+ line-height: 1.2;
+}
+.milestone-countdown,
+.milestone-message {
+ background-color: <?php echo self::sanitize_color_hex( $colors['bg'] ); ?>;
+ border: 1px solid <?php echo self::sanitize_color_hex( $colors['border'] ); ?>;
+ border-top: 0;
+ color: <?php echo self::sanitize_color_hex( $colors['text'] ); ?>;
+ padding-bottom: 1em;
+}
+.milestone-message {
+ padding-top: 1em
+}
+</style>
+<?php
+ }
+
+ /**
+ * Ensure that a string representing a color in hexadecimal
+ * notation is safe for use in css and database saves.
+ *
+ * @param string Color in hexadecimal notation. "#" may or may not be prepended to the string.
+ * @return string Color in hexadecimal notation on success - the string "transparent" otherwise.
+ */
+ public static function sanitize_color_hex( $hex, $prefix = '#' ) {
+ $hex = trim( $hex );
+
+ /* Strip recognized prefixes. */
+ if ( 0 === strpos( $hex, '#' ) ) {
+ $hex = substr( $hex, 1 );
+ } elseif ( 0 === strpos( $hex, '%23' ) ) {
+ $hex = substr( $hex, 3 );
+ }
+
+ if ( 0 !== preg_match( '/^[0-9a-fA-F]{6}$/', $hex ) ) {
+ return $prefix . $hex;
+ }
+
+ return 'transparent';
+ }
+
+ /**
+ * Localize Front-end Script.
+ *
+ * Print the javascript configuration array only if the
+ * current template has an instance of the widget that
+ * is still counting down. In all other cases, this
+ * function will dequeue milestone.js.
+ *
+ * Hooks into the "wp_footer" action.
+ */
+ function localize_script() {
+ if ( Jetpack_AMP_Support::is_amp_request() ) {
+ return;
+ }
+
+ if ( empty( self::$config_js['instances'] ) ) {
+ wp_dequeue_script( 'milestone' );
+ return;
+ }
+ self::$config_js['api_root'] = esc_url_raw( rest_url() );
+ wp_localize_script( 'milestone', 'MilestoneConfig', self::$config_js );
+ }
+
+ /**
+ * Widget
+ */
+ function widget( $args, $instance ) {
+ echo $args['before_widget'];
+
+ /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
+ $title = apply_filters( 'widget_title', $instance['title'] );
+ if ( ! empty( $title ) ) {
+ echo $args['before_title'] . $title . $args['after_title'];
+ }
+
+ $data = $this->get_widget_data( $instance );
+
+ self::$config_js['instances'][] = array(
+ 'id' => $args['widget_id'],
+ 'message' => $data['message'],
+ 'refresh' => $data['refresh']
+ );
+
+ echo '<div class="milestone-content">';
+
+ echo '<div class="milestone-header">';
+ echo '<strong class="event">' . esc_html( $instance['event'] ) . '</strong>';
+ echo '<span class="date">' . esc_html( date_i18n( get_option( 'date_format' ), $data['milestone'] ) ) . '</span>';
+ echo '</div>';
+
+ echo $data['message'];
+
+ echo '</div><!--milestone-content-->';
+
+ echo $args['after_widget'];
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'milestone' );
+ }
+
+ function get_widget_data( $instance ) {
+ $data = array();
+
+ $instance = $this->sanitize_instance( $instance );
+
+ $milestone = mktime( $instance['hour'], $instance['min'], 0, $instance['month'], $instance['day'], $instance['year'] );
+ $now = (int) current_time( 'timestamp' );
+ $type = $instance['type'];
+
+ if ( 'since' === $type ) {
+ $diff = (int) floor( $now - $milestone );
+ } else {
+ $diff = (int) floor( $milestone - $now );
+ }
+
+ $data['diff'] = $diff;
+ $data['unit'] = $this->get_unit( $diff, $instance['unit'] );
+
+ // Setting the refresh counter to equal the number of seconds it takes to flip a unit
+ $refresh_intervals = array(
+ 0, // should be YEAR_IN_SECONDS, but doing setTimeout for a year doesn't seem to be logical
+ 0, // same goes for MONTH_IN_SECONDS,
+ DAY_IN_SECONDS,
+ HOUR_IN_SECONDS,
+ MINUTE_IN_SECONDS,
+ 1
+ );
+
+ $data['refresh'] = $refresh_intervals[ array_search( $data['unit'], $this->available_units ) ];
+ $data['milestone'] = $milestone;
+
+ if ( ( 1 > $diff ) && ( 'until' === $type ) ) {
+ $data['message'] = '<div class="milestone-message">' . $instance['message'] . '</div>';
+ $data['refresh'] = 0; // No need to refresh, the milestone has been reached
+ } else {
+ $interval_text = $this->get_interval_in_units( $diff, $data['unit'] );
+ $interval = intval( $interval_text );
+
+ if ( 'since' === $type ) {
+
+ switch ( $data['unit'] ) {
+ case 'years':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">year ago.</span>',
+ '<span class="difference">%s</span> <span class="label">years ago.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ case 'months':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">month ago.</span>',
+ '<span class="difference">%s</span> <span class="label">months ago.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ case 'days':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">day ago.</span>',
+ '<span class="difference">%s</span> <span class="label">days ago.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ case 'hours':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">hour ago.</span>',
+ '<span class="difference">%s</span> <span class="label">hours ago.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ case 'minutes':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">minute ago.</span>',
+ '<span class="difference">%s</span> <span class="label">minutes ago.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ case 'seconds':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">second ago.</span>',
+ '<span class="difference">%s</span> <span class="label">seconds ago.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ }
+ } else {
+ switch ( $this->get_unit( $diff, $instance['unit'] ) ) {
+ case 'years':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">year to go.</span>',
+ '<span class="difference">%s</span> <span class="label">years to go.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ case 'months':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">month to go.</span>',
+ '<span class="difference">%s</span> <span class="label">months to go.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ case 'days':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">day to go.</span>',
+ '<span class="difference">%s</span> <span class="label">days to go.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ case 'hours':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">hour to go.</span>',
+ '<span class="difference">%s</span> <span class="label">hours to go.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ case 'minutes':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">minute to go.</span>',
+ '<span class="difference">%s</span> <span class="label">minutes to go.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ case 'seconds':
+ $data['message'] = sprintf(
+ _n(
+ '<span class="difference">%s</span> <span class="label">second to go.</span>',
+ '<span class="difference">%s</span> <span class="label">seconds to go.</span>',
+ $interval,
+ 'jetpack'
+ ),
+ $interval_text
+ );
+ break;
+ }
+ }
+ $data['message'] = '<div class="milestone-countdown">' . $data['message'] . '</div>';
+ }
+
+ return $data;
+ }
+
+ /**
+ * Return the largest possible time unit that the difference will be displayed in.
+ *
+ * @param Integer $seconds the interval in seconds
+ * @param String $maximum_unit the maximum unit that will be used. Optional.
+ * @return String $calculated_unit
+ */
+ protected function get_unit( $seconds, $maximum_unit = 'automatic' ) {
+ $unit = '';
+
+ if ( $seconds >= YEAR_IN_SECONDS * 2 ) {
+ // more than 2 years - show in years, one decimal point
+ $unit = 'years';
+
+ } else if ( $seconds >= YEAR_IN_SECONDS ) {
+ if ( 'years' === $maximum_unit ) {
+ $unit = 'years';
+ } else {
+ // automatic mode - showing months even if it's between one and two years
+ $unit = 'months';
+ }
+
+ } else if ( $seconds >= MONTH_IN_SECONDS * 3 ) {
+ // fewer than 2 years - show in months
+ $unit = 'months';
+
+ } else if ( $seconds >= MONTH_IN_SECONDS ) {
+ if ( 'months' === $maximum_unit ) {
+ $unit = 'months';
+ } else {
+ // automatic mode - showing days even if it's between one and three months
+ $unit = 'days';
+ }
+
+ } else if ( $seconds >= DAY_IN_SECONDS - 1 ) {
+ // fewer than a month - show in days
+ $unit = 'days';
+
+ } else if ( $seconds >= HOUR_IN_SECONDS - 1 ) {
+ // less than 1 day - show in hours
+ $unit = 'hours';
+
+ } else if ( $seconds >= MINUTE_IN_SECONDS - 1 ) {
+ // less than 1 hour - show in minutes
+ $unit = 'minutes';
+
+ } else {
+ // less than 1 minute - show in seconds
+ $unit = 'seconds';
+ }
+
+ $maximum_unit_index = array_search( $maximum_unit, $this->available_units );
+ $unit_index = array_search( $unit, $this->available_units );
+
+ if (
+ false === $maximum_unit_index // the maximum unit parameter is automatic
+ || $unit_index > $maximum_unit_index // there is not enough seconds for even one maximum time unit
+ ) {
+ return $unit;
+ }
+ return $maximum_unit;
+ }
+
+ /**
+ * Returns a time difference value in specified units.
+ *
+ * @param Integer $seconds
+ * @param String $units
+ * @return Integer|String $time_in_units
+ */
+ protected function get_interval_in_units( $seconds, $units ) {
+ switch ( $units ) {
+ case 'years':
+ $years = $seconds / YEAR_IN_SECONDS;
+ $decimals = abs( round( $years, 1 ) - round( $years ) ) > 0 ? 1 : 0;
+ return number_format_i18n( $years, $decimals );
+ case 'months':
+ return (int) ( $seconds / 60 / 60 / 24 / 30 );
+ case 'days':
+ return (int) ( $seconds / 60 / 60 / 24 + 1 );
+ case 'hours':
+ return (int) ( $seconds / 60 / 60 );
+ case 'minutes':
+ return (int) ( $seconds / 60 + 1 );
+ default:
+ return $seconds;
+ }
+ }
+
+ /**
+ * Update
+ */
+ function update( $new_instance, $old_instance ) {
+ return $this->sanitize_instance( $new_instance );
+ }
+
+ /*
+ * Make sure that a number is within a certain range.
+ * If the number is too small it will become the possible lowest value.
+ * If the number is too large it will become the possible highest value.
+ *
+ * @param int $n The number to check.
+ * @param int $floor The lowest possible value.
+ * @param int $ceil The highest possible value.
+ */
+ function sanitize_range( $n, $floor, $ceil ) {
+ $n = (int) $n;
+ if ( $n < $floor ) {
+ $n = $floor;
+ } elseif ( $n > $ceil ) {
+ $n = $ceil;
+ }
+ return $n;
+ }
+
+ /*
+ * Sanitize an instance of this widget.
+ *
+ * Date ranges match the documentation for mktime in the php manual.
+ * @see http://php.net/manual/en/function.mktime.php#refsect1-function.mktime-parameters
+ *
+ * @uses Milestone_Widget::sanitize_range().
+ */
+ function sanitize_instance( $dirty ) {
+ $now = (int) current_time( 'timestamp' );
+
+ $dirty = wp_parse_args( $dirty, array(
+ 'title' => '',
+ 'event' => __( 'The Big Day', 'jetpack' ),
+ 'unit' => 'automatic',
+ 'type' => 'until',
+ 'message' => __( 'The big day is here.', 'jetpack' ),
+ 'day' => date( 'd', $now ),
+ 'month' => date( 'm', $now ),
+ 'year' => date( 'Y', $now ),
+ 'hour' => 0,
+ 'min' => 0,
+ ) );
+
+ $allowed_tags = array(
+ 'a' => array( 'title' => array(), 'href' => array(), 'target' => array() ),
+ 'em' => array( 'title' => array() ),
+ 'strong' => array( 'title' => array() ),
+ );
+
+ $clean = array(
+ 'title' => trim( strip_tags( stripslashes( $dirty['title'] ) ) ),
+ 'event' => trim( strip_tags( stripslashes( $dirty['event'] ) ) ),
+ 'unit' => $dirty['unit'],
+ 'type' => $dirty['type'],
+ 'message' => wp_kses( $dirty['message'], $allowed_tags ),
+ 'year' => $this->sanitize_range( $dirty['year'], 1901, 2037 ),
+ 'month' => $this->sanitize_range( $dirty['month'], 1, 12 ),
+ 'hour' => $this->sanitize_range( $dirty['hour'], 0, 23 ),
+ 'min' => zeroise( $this->sanitize_range( $dirty['min'], 0, 59 ), 2 ),
+ );
+
+ $clean['day'] = $this->sanitize_range( $dirty['day'], 1, date( 't', mktime( 0, 0, 0, $clean['month'], 1, $clean['year'] ) ) );
+
+ return $clean;
+ }
+
+ /**
+ * Form
+ */
+ function form( $instance ) {
+ $instance = $this->sanitize_instance( $instance );
+
+ $units = array(
+ 'automatic' => _x( 'Automatic', 'Milestone widget: mode in which the date unit is determined automatically', 'jetpack' ),
+ 'years' => _x( 'Years', 'Milestone widget: mode in which the date unit is set to years', 'jetpack' ),
+ 'months' => _x( 'Months', 'Milestone widget: mode in which the date unit is set to months', 'jetpack' ),
+ 'days' => _x( 'Days', 'Milestone widget: mode in which the date unit is set to days', 'jetpack' ),
+ 'hours' => _x( 'Hours', 'Milestone widget: mode in which the date unit is set to hours', 'jetpack' ),
+ );
+ ?>
+
+ <div class="milestone-widget">
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'event' ); ?>"><?php _e( 'Description', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'event' ); ?>" name="<?php echo $this->get_field_name( 'event' ); ?>" type="text" value="<?php echo esc_attr( $instance['event'] ); ?>" />
+ </p>
+
+ <fieldset class="jp-ms-data-time">
+ <legend><?php esc_html_e( 'Date', 'jetpack' ); ?></legend>
+
+ <label for="<?php echo $this->get_field_id( 'month' ); ?>" class="assistive-text"><?php _e( 'Month', 'jetpack' ); ?></label>
+ <select id="<?php echo $this->get_field_id( 'month' ); ?>" class="month" name="<?php echo $this->get_field_name( 'month' ); ?>"><?php
+ global $wp_locale;
+ for ( $i = 1; $i < 13; $i++ ) {
+ $monthnum = zeroise( $i, 2 );
+ echo '<option value="' . esc_attr( $monthnum ) . '"' . selected( $i, $instance['month'], false ) . '>' . $monthnum . '-' . $wp_locale->get_month_abbrev( $wp_locale->get_month( $i ) ) . '</option>';
+ }
+ ?></select>
+
+ <label for="<?php echo $this->get_field_id( 'day' ); ?>" class="assistive-text"><?php _e( 'Day', 'jetpack' ); ?></label>
+ <input id="<?php echo $this->get_field_id( 'day' ); ?>" class="day" name="<?php echo $this->get_field_name( 'day' ); ?>" type="text" value="<?php echo esc_attr( $instance['day'] ); ?>">,
+
+ <label for="<?php echo $this->get_field_id( 'year' ); ?>" class="assistive-text"><?php _e( 'Year', 'jetpack' ); ?></label>
+ <input id="<?php echo $this->get_field_id( 'year' ); ?>" class="year" name="<?php echo $this->get_field_name( 'year' ); ?>" type="text" value="<?php echo esc_attr( $instance['year'] ); ?>">
+ </fieldset>
+
+ <fieldset class="jp-ms-data-time">
+ <legend><?php esc_html_e( 'Time', 'jetpack' ); ?></legend>
+
+ <label for="<?php echo $this->get_field_id( 'hour' ); ?>" class="assistive-text"><?php _e( 'Hour', 'jetpack' ); ?></label>
+ <input id="<?php echo $this->get_field_id( 'hour' ); ?>" class="hour" name="<?php echo $this->get_field_name( 'hour' ); ?>" type="text" value="<?php echo esc_attr( $instance['hour'] ); ?>">
+
+ <label for="<?php echo $this->get_field_id( 'min' ); ?>" class="assistive-text"><?php _e( 'Minutes', 'jetpack' ); ?></label>
+
+ <span class="time-separator">:</span>
+
+ <input id="<?php echo $this->get_field_id( 'min' ); ?>" class="minutes" name="<?php echo $this->get_field_name( 'min' ); ?>" type="text" value="<?php echo esc_attr( $instance['min'] ); ?>">
+ </fieldset>
+
+ <fieldset class="jp-ms-data-unit">
+ <legend><?php esc_html_e( 'Time Unit', 'jetpack' ); ?></legend>
+
+ <label for="<?php echo $this->get_field_id( 'unit' ); ?>" class="assistive-text">
+ <?php _e( 'Time Unit', 'jetpack' ); ?>
+ </label>
+ <select id="<?php echo $this->get_field_id( 'unit' ); ?>" class="unit" name="<?php echo $this->get_field_name( 'unit' ); ?>">
+ <?php
+ foreach ( $units as $key => $unit ) {
+ echo '<option value="' . esc_attr( $key ) . '"' . selected( $key, $instance['unit'], false ) . '>' . $unit . '</option>';
+ }
+ ?></select>
+ </fieldset>
+
+ <ul class="milestone-type">
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['type'], 'until' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'type' ) ); ?>"
+ type="radio"
+ value="until"
+ />
+ <?php esc_html_e( 'Until your milestone', 'jetpack' ); ?>
+ </label>
+ </li>
+
+ <li>
+ <label>
+ <input
+ <?php checked( $instance['type'], 'since' ); ?>
+ name="<?php echo esc_attr( $this->get_field_name( 'type' ) ); ?>"
+ type="radio"
+ value="since"
+ />
+ <?php esc_html_e( 'Since your milestone', 'jetpack' ); ?>
+ </label>
+ </li>
+ </ul>
+
+ <p class="milestone-message-wrapper">
+ <label for="<?php echo $this->get_field_id( 'message' ); ?>"><?php _e( 'Milestone Reached Message', 'jetpack' ); ?></label>
+ <textarea id="<?php echo $this->get_field_id( 'message' ); ?>" name="<?php echo $this->get_field_name( 'message' ); ?>" class="widefat" rows="3"><?php echo esc_textarea( $instance['message'] ); ?></textarea>
+ </p>
+ </div>
+
+ <?php
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/milestone/style-admin.css b/plugins/jetpack/modules/widgets/milestone/style-admin.css
new file mode 100644
index 00000000..15a97102
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/milestone/style-admin.css
@@ -0,0 +1,50 @@
+.milestone-widget fieldset {
+ margin-bottom: 1em;
+}
+
+.milestone-widget fieldset * {
+ vertical-align: middle;
+}
+
+.jp-ms-data-time input[type="text"] {
+ text-align: right;
+ width: 2.1em;
+}
+
+.jp-ms-data-time .month {
+ width: 5.4em;
+
+}
+
+.jp-ms-data-time .year[type="text"] {
+ text-align: right;
+ width: 3.2em;
+}
+
+.jp-ms-data-time input[type="text"] {
+ text-align: right;
+ width: 3.2em;
+}
+
+.jp-ms-data-time .year[type="text"] {
+ width: 4.5em;
+}
+
+.jp-ms-data-time .assistive-text,
+.jp-ms-data-unit .assistive-text {
+ position: absolute !important;
+ clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
+ clip: rect(1px, 1px, 1px, 1px);
+}
+
+@media screen and (max-width: 782px) {
+ .jp-ms-data-time input[type="text"],
+ .jp-ms-data-time .year[type="text"] {
+ width: 2.8em;
+
+ }
+
+ .jp-ms-data-time .year[type="text"] {
+ width: 4em;
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/my-community.php b/plugins/jetpack/modules/widgets/my-community.php
new file mode 100644
index 00000000..03958c38
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/my-community.php
@@ -0,0 +1,297 @@
+<?php
+/**
+ * Disable direct access/execution to/of the widget code.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Jetpack_My_Community_Widget displays community members of this site.
+ *
+ * A community member is a WordPress.com user that liked or commented on an entry or subscribed to the site.
+ * Requires WordPress.com connection to work. Otherwise it won't be visible in Widgets screen in admin.
+ */
+class Jetpack_My_Community_Widget extends WP_Widget {
+ /**
+ * Transient expiration time.
+ *
+ * @var int $expiration
+ */
+ static $expiration = 600;
+
+ /**
+ * Default widget title.
+ *
+ * @var string $default_title
+ */
+ var $default_title;
+
+ /**
+ * Registers the widget with WordPress.
+ */
+ function __construct() {
+ parent::__construct(
+ 'jetpack_my_community', // Base ID
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', esc_html__( 'My Community', 'jetpack' ) ),
+ array(
+ 'description' => esc_html__( "Display members of your site's community.", 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+
+ if ( is_active_widget( false, false, $this->id_base ) || is_active_widget( false, false, 'monster' ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_style' ) );
+ }
+
+ $this->default_title = esc_html__( 'Community', 'jetpack' );
+ }
+
+ /**
+ * Enqueue stylesheet for grid layout.
+ */
+ function enqueue_style() {
+ wp_register_style( 'jetpack-my-community-widget', plugins_url( 'my-community/style.css', __FILE__ ), array(), '20160129' );
+ wp_enqueue_style( 'jetpack-my-community-widget' );
+ }
+
+ /**
+ * Back end widget form.
+ *
+ * @see WP_Widget::form()
+ *
+ * @param array $instance Previously saved values from database.
+ *
+ * @return string|void
+ */
+ function form( $instance ) {
+ $title = isset( $instance['title'] ) ? $instance['title'] : false;
+ if ( false === $title ) {
+ $title = $this->default_title;
+ }
+
+ $number = isset( $instance['number'] ) ? $instance['number'] : 10;
+ if ( ! in_array( $number, array( 10, 50 ) ) ) {
+ $number = 10;
+ }
+
+ $include_likers = isset( $instance['include_likers'] ) ? (bool) $instance['include_likers'] : true;
+ $include_followers = isset( $instance['include_followers'] ) ? (bool) $instance['include_followers'] : true;
+ $include_commenters = isset( $instance['include_commenters'] ) ? (bool) $instance['include_commenters'] : true;
+ ?>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php esc_html_e( 'Title:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
+ </p>
+
+ <p>
+ <label><?php esc_html_e( 'Show a maximum of', 'jetpack' ); ?></label>
+ </p>
+ <ul>
+ <li><label><input id="<?php echo $this->get_field_id( 'number' ); ?>-few" name="<?php echo $this->get_field_name( 'number' ); ?>" type="radio" value="10" <?php checked( '10', $number ); ?> /> <?php esc_html_e( '10 community members', 'jetpack' ); ?></label></li>
+ <li><label><input id="<?php echo $this->get_field_id( 'number' ); ?>-lots" name="<?php echo $this->get_field_name( 'number' ); ?>" type="radio" value="50" <?php checked( '50', $number ); ?> /> <?php esc_html_e( '50 community members', 'jetpack' ); ?></label></li>
+ </ul>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'include_likers' ); ?>">
+ <input type="checkbox" class="checkbox" id="<?php echo $this->get_field_id( 'include_likers' ); ?>" name="<?php echo $this->get_field_name( 'include_likers' ); ?>" value="1" <?php checked( $include_likers, 1 ); ?> />
+ <?php esc_html_e( 'Include activity from likers', 'jetpack' ); ?>
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'include_followers' ); ?>">
+ <input type="checkbox" class="checkbox" id="<?php echo $this->get_field_id( 'include_followers' ); ?>" name="<?php echo $this->get_field_name( 'include_followers' ); ?>" value="1" <?php checked( $include_followers, 1 ); ?> />
+ <?php esc_html_e( 'Include activity from followers', 'jetpack' ); ?>
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'include_commenters' ); ?>">
+ <input type="checkbox" class="checkbox" id="<?php echo $this->get_field_id( 'include_commenters' ); ?>" name="<?php echo $this->get_field_name( 'include_commenters' ); ?>" value="1" <?php checked( $include_commenters, 1 ); ?> />
+ <?php esc_html_e( 'Include activity from commenters', 'jetpack' ); ?>
+ </label>
+ </p>
+
+ <?php
+ }
+
+ /**
+ * Sanitize widget form values as they are saved.
+ *
+ * @see WP_Widget::update()
+ *
+ * @param array $new_instance Values just sent to be saved.
+ * @param array $old_instance Previously saved values from database.
+ *
+ * @return array Updated safe values to be saved.
+ */
+ function update( $new_instance, $old_instance ) {
+ $instance = array();
+ $instance['title'] = wp_kses( $new_instance['title'], array() );
+ if ( $instance['title'] === $this->default_title ) {
+ $instance['title'] = false; // Store as false in case of language change
+ }
+
+ $instance['number'] = (int) $new_instance['number'];
+ if ( ! in_array( $instance['number'], array( 10, 50 ) ) ) {
+ $instance['number'] = 10;
+ }
+
+ $instance['include_likers'] = (bool) $new_instance['include_likers'];
+ $instance['include_followers'] = (bool) $new_instance['include_followers'];
+ $instance['include_commenters'] = (bool) $new_instance['include_commenters'];
+
+ delete_transient( "$this->id-v2-{$instance['number']}" . (int) $instance['include_likers'] . (int) $instance['include_followers'] . (int) $instance['include_commenters'] );
+
+ return $instance;
+ }
+
+ /**
+ * Front-end display of widget.
+ *
+ * @see WP_Widget::widget()
+ *
+ * @param array $args Widget arguments.
+ * @param array $instance Saved values from database.
+ */
+ function widget( $args, $instance ) {
+ $instance = wp_parse_args(
+ $instance, array(
+ 'title' => false,
+ 'number' => true,
+ 'include_likers' => true,
+ 'include_followers' => true,
+ 'include_commenters' => true,
+ )
+ );
+
+ $title = $instance['title'];
+
+ if ( false === $title ) {
+ $title = $this->default_title;
+ }
+
+ /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
+ $title = apply_filters( 'widget_title', $title );
+
+ echo $args['before_widget'];
+
+ if ( ! empty( $title ) ) {
+ echo $args['before_title'] . $title . $args['after_title'];
+ }
+
+ $transient_name = "$this->id-v2-{$instance['number']}" . (int) $instance['include_likers'] . (int) $instance['include_followers'] . (int) $instance['include_commenters'];
+
+ $my_community = get_transient( $transient_name );
+
+ if ( empty( $my_community ) ) {
+ $my_community = $this->get_community( $instance );
+
+ set_transient( $transient_name, $my_community, self::$expiration );
+ }
+
+ echo $my_community;
+
+ echo $args['after_widget'];
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'my_community' );
+ }
+
+ /**
+ * Initiate request and render the response.
+ *
+ * @since 4.0
+ *
+ * @param array $query
+ *
+ * @return string
+ */
+ function get_community( $query ) {
+ $members = $this->fetch_remote_community( $query );
+
+ if ( ! empty( $members ) ) {
+
+ $my_community = '<div class="widgets-multi-column-grid"><ul>';
+
+ foreach ( $members as $member ) {
+ $my_community .= sprintf(
+ '<li><a href="%s" title="%s"><img alt="%s" src="%s" class="avatar avatar-48" height="48" width="48"></a></li>',
+ esc_url( $member->profile_URL ),
+ esc_attr( $member->name ),
+ esc_attr( $member->name ),
+ esc_url( $member->avatar_URL )
+ );
+ }
+
+ $my_community .= '</ul></div>';
+
+ } else {
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ $my_community = '<p>' . wp_kses(
+ sprintf(
+ __( 'There are no users to display in this <a href="%1$s">My Community widget</a>. <a href="%2$s">Want more traffic?</a>', 'jetpack' ),
+ admin_url( 'widgets.php' ),
+ 'https://jetpack.com/support/getting-more-views-and-traffic/'
+ ), array( 'a' => array( 'href' => true ) )
+ ) . '</p>';
+ } else {
+ $my_community = '<p>' . esc_html__( "I'm just starting out; leave me a comment or a like :)", 'jetpack' ) . '</p>';
+ }
+ }
+
+ return $my_community;
+ }
+
+ /**
+ * Request community members to WordPress.com endpoint.
+ *
+ * @since 4.0
+ *
+ * @param $query
+ *
+ * @return array
+ */
+ function fetch_remote_community( $query ) {
+ $jetpack_blog_id = Jetpack_Options::get_option( 'id' );
+ $url = add_query_arg(
+ array(
+ 'number' => $query['number'],
+ 'likers' => (int) $query['include_likers'],
+ 'followers' => (int) $query['include_followers'],
+ 'commenters' => (int) $query['include_commenters'],
+ ),
+ "https://public-api.wordpress.com/rest/v1.1/sites/$jetpack_blog_id/community"
+ );
+ $response = wp_remote_get( $url );
+ $response_body = wp_remote_retrieve_body( $response );
+
+ if ( empty( $response_body ) ) {
+ return array();
+ }
+
+ $response_body = json_decode( $response_body );
+
+ if ( isset( $response_body->users ) ) {
+ return $response_body->users;
+ }
+
+ return array();
+ }
+}
+
+/**
+ * If site is connected to WordPress.com, register the widget.
+ *
+ * @since 4.0
+ */
+function jetpack_my_community_init() {
+ if ( Jetpack::is_active() ) {
+ register_widget( 'Jetpack_My_Community_Widget' );
+ }
+}
+
+add_action( 'widgets_init', 'jetpack_my_community_init' );
diff --git a/plugins/jetpack/modules/widgets/my-community/style.css b/plugins/jetpack/modules/widgets/my-community/style.css
new file mode 100644
index 00000000..5616e890
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/my-community/style.css
@@ -0,0 +1,35 @@
+/* Multi-Column Grid Layout */
+.widgets-multi-column-grid ul {
+ overflow: hidden;
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+}
+
+.widgets-multi-column-grid ul li {
+ background: none;
+ clear: none;
+ float: left;
+ margin: 0 -5px -3px 0;
+ padding: 0 8px 6px 0;
+ border: none;
+ list-style-type: none !important;
+}
+
+.widgets-multi-column-grid ul li a {
+ background: none;
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+
+.widgets-multi-column-grid .avatar {
+ vertical-align: middle;
+}
+
+/* Ensure My Community images fit the 48 pixel grid. */
+.widget_jetpack_my_community .avatar-48,
+.widget_jetpack_my_community .avatar-240 {
+ max-width: 48px;
+ max-height: 48px;
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets/rsslinks-widget.php b/plugins/jetpack/modules/widgets/rsslinks-widget.php
new file mode 100644
index 00000000..01d1ee2d
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/rsslinks-widget.php
@@ -0,0 +1,242 @@
+<?php
+/**
+ * Module Name: RSS Links Widget
+ * Module Description: Easily add RSS links to your theme's sidebar.
+ * Sort Order: 20
+ * First Introduced: 1.2
+ */
+
+class Jetpack_RSS_Links_Widget extends WP_Widget {
+
+ function __construct() {
+ $widget_ops = array(
+ 'classname' => 'widget_rss_links',
+ 'description' => __( "Links to your blog's RSS feeds", 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ );
+ parent::__construct(
+ 'rss_links',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'RSS Links', 'jetpack' ) ),
+ $widget_ops
+ );
+ }
+
+ function widget( $args, $instance ) {
+ $instance = wp_parse_args( (array) $instance, $this->defaults() );
+
+ extract( $args );
+
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', $instance['title'] );
+ echo $before_widget;
+
+ if ( $title ) {
+ echo $before_title . stripslashes( $title ) . $after_title;
+ }
+
+ if ( 'text' == $instance['format'] ) {
+ echo '<ul>';
+ }
+
+ if ( 'posts' == $instance['display'] ) {
+ $this->_rss_link( 'posts', $instance );
+ } elseif ( 'comments' == $instance['display'] ) {
+ $this->_rss_link( 'comments', $instance );
+ } elseif ( 'posts-comments' == $instance['display'] ) {
+ $this->_rss_link( 'posts', $instance );
+ $this->_rss_link( 'comments', $instance );
+ }
+
+ if ( 'text' == $instance['format'] ) {
+ echo '</ul>';
+ }
+
+ echo "\n" . $after_widget;
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'rss-links' );
+ }
+
+ /**
+ * Return an associative array of default values
+ * These values are used in new widgets as well as when sanitizing input.
+ *
+ * @return array Array of default values for the Widget's options
+ */
+ function defaults() {
+ return array(
+ 'title' => '',
+ 'display' => 'posts-comments',
+ 'format' => 'text',
+ );
+ }
+
+ function update( $new_instance, $old_instance ) {
+ $instance = $old_instance;
+
+ $instance['title'] = wp_filter_nohtml_kses( $new_instance['title'] );
+ $instance['display'] = $new_instance['display'];
+ $instance['format'] = $new_instance['format'];
+ $instance['imagesize'] = $new_instance['imagesize'];
+ $instance['imagecolor'] = $new_instance['imagecolor'];
+
+ return $instance;
+ }
+
+ function form( $instance ) {
+ $instance = wp_parse_args( (array) $instance, $this->defaults() );
+
+ $title = stripslashes( $instance['title'] );
+ $display = $instance['display'];
+ $format = $instance['format'];
+ $image_size = isset( $instance['imagesize'] ) ? $instance['imagesize'] : 0;
+ $image_color = isset( $instance['imagecolor'] ) ? $instance['imagecolor'] : 'red';
+
+ echo '<p><label for="' . $this->get_field_id( 'title' ) . '">' . esc_html__( 'Title:', 'jetpack' ) . '
+ <input class="widefat" id="' . $this->get_field_id( 'title' ) . '" name="' . $this->get_field_name( 'title' ) . '" type="text" value="' . esc_attr( $title ) . '" />
+ </label></p>';
+
+ $displays = array(
+ 'posts' => __( 'Posts', 'jetpack' ),
+ 'comments' => __( 'Comments', 'jetpack' ),
+ 'posts-comments' => __( 'Posts & Comments', 'jetpack' ),
+ );
+ echo '<p><label for="' . $this->get_field_id( 'display' ) . '">' . esc_html__( 'Feed(s) to Display:', 'jetpack' ) . '
+ <select class="widefat" id="' . $this->get_field_id( 'display' ) . '" name="' . $this->get_field_name( 'display' ) . '">';
+ foreach ( $displays as $display_option => $label ) {
+ echo '<option value="' . esc_attr( $display_option ) . '"';
+ if ( $display_option == $display ) {
+ echo ' selected="selected"';
+ }
+ echo '>' . esc_html( $label ) . '</option>' . "\n";
+ }
+ echo '</select></label></p>';
+
+ $formats = array(
+ 'text' => __( 'Text Link', 'jetpack' ),
+ 'image' => __( 'Image Link', 'jetpack' ),
+ 'text-image' => __( 'Text & Image Links', 'jetpack' ),
+ );
+ echo '<p><label for="' . $this->get_field_id( 'format' ) . '">' . _x( 'Format:', 'Noun', 'jetpack' ) . '
+ <select class="widefat" id="' . $this->get_field_id( 'format' ) . '" name="' . $this->get_field_name( 'format' ) . '" onchange="if ( this.value == \'text\' ) jQuery( \'#' . $this->get_field_id( 'image-settings' ) . '\' ).fadeOut(); else jQuery( \'#' . $this->get_field_id( 'image-settings' ) . '\' ).fadeIn();">';
+ foreach ( $formats as $format_option => $label ) {
+ echo '<option value="' . esc_attr( $format_option ) . '"';
+ if ( $format_option == $format ) {
+ echo ' selected="selected"';
+ }
+ echo '>' . esc_html( $label ) . '</option>' . "\n";
+ }
+ echo '</select></label></p>';
+
+ echo '<div id="' . $this->get_field_id( 'image-settings' ) . '"';
+ if ( 'text' == $format ) {
+ echo ' style="display: none;"';
+ }
+ echo '><h3>' . esc_html__( 'Image Settings:', 'jetpack' ) . '</h3>';
+
+ $sizes = array(
+ 'small' => __( 'Small', 'jetpack' ),
+ 'medium' => __( 'Medium', 'jetpack' ),
+ 'large' => __( 'Large', 'jetpack' ),
+ );
+ echo '<p><label for="' . $this->get_field_id( 'imagesize' ) . '">' . esc_html__( 'Image Size:', 'jetpack' ) . '
+ <select class="widefat" id="' . $this->get_field_id( 'imagesize' ) . '" name="' . $this->get_field_name( 'imagesize' ) . '">';
+ foreach ( $sizes as $size => $label ) {
+ echo '<option value="' . esc_attr( $size ) . '"';
+ if ( $size == $image_size ) {
+ echo ' selected="selected"';
+ }
+ echo '>' . esc_html( $label ) . '</option>' . "\n";
+ }
+ echo '</select></label></p>';
+
+ $colors = array(
+ 'red' => __( 'Red', 'jetpack' ),
+ 'orange' => __( 'Orange', 'jetpack' ),
+ 'green' => __( 'Green', 'jetpack' ),
+ 'blue' => __( 'Blue', 'jetpack' ),
+ 'purple' => __( 'Purple', 'jetpack' ),
+ 'pink' => __( 'Pink', 'jetpack' ),
+ 'silver' => __( 'Silver', 'jetpack' ),
+ );
+ echo '<p><label for="' . $this->get_field_id( 'imagecolor' ) . '">' . esc_html__( 'Image Color:', 'jetpack' ) . '
+ <select class="widefat" id="' . $this->get_field_id( 'imagecolor' ) . '" name="' . $this->get_field_name( 'imagecolor' ) . '">';
+ foreach ( $colors as $color => $label ) {
+ echo '<option value="' . esc_attr( $color ) . '"';
+ if ( $color == $image_color ) {
+ echo ' selected="selected"';
+ }
+ echo '>' . esc_html( $label ) . '</option>' . "\n";
+ }
+ echo '</select></label></p></div>';
+ }
+
+ function _rss_link( $type = 'posts', $args ) {
+ if ( 'posts' == $type ) {
+ $type_text = __( 'Posts', 'jetpack' );
+ $rss_type = 'rss2_url';
+ } elseif ( 'comments' == $type ) {
+ $type_text = __( 'Comments', 'jetpack' );
+ $rss_type = 'comments_rss2_url';
+ }
+
+ $subscribe_to = sprintf( __( 'Subscribe to %s', 'jetpack' ), $type_text );
+
+ $link_item = '';
+ $format = $args['format'];
+
+ /**
+ * Filters the target link attribute for the RSS link in the RSS widget.
+ *
+ * @module widgets
+ *
+ * @since 3.4.0
+ *
+ * @param bool false Control whether the link should open in a new tab. Default to false.
+ */
+ if ( apply_filters( 'jetpack_rsslinks_widget_target_blank', false ) ) {
+ $link_target = '_blank';
+ } else {
+ $link_target = '_self';
+ }
+
+ if ( 'image' == $format || 'text-image' == $format ) {
+ /**
+ * Filters the image used as RSS icon in the RSS widget.
+ *
+ * @module widgets
+ *
+ * @since 3.6.0
+ *
+ * @param string $var URL of RSS Widget icon.
+ */
+ $link_image = apply_filters( 'jetpack_rss_widget_icon', plugins_url( 'images/rss/' . $args['imagecolor'] . '-' . $args['imagesize'] . '.png', dirname( dirname( __FILE__ ) ) ) );
+ $link_item = '<a target="' . $link_target . '" href="' . get_bloginfo( $rss_type ) . '" title="' . esc_attr( $subscribe_to ) . '"><img src="' . esc_url( $link_image ) . '" alt="RSS Feed" /></a>';
+ }
+ if ( 'text-image' == $format ) {
+ $link_item .= '&nbsp;<a target="' . $link_target . '" href="' . get_bloginfo( $rss_type ) . '" title="' . esc_attr( $subscribe_to ) . '">' . esc_html__( 'RSS - ' . $type_text, 'jetpack' ) . '</a>';
+ }
+ if ( 'text' == $format ) {
+ $link_item = '<a target="' . $link_target . '" href="' . get_bloginfo( $rss_type ) . '" title="' . esc_attr( $subscribe_to ) . '">' . esc_html__( 'RSS - ' . $type_text, 'jetpack' ) . '</a>';
+ }
+
+ if ( 'text' == $format ) {
+ echo '<li>';
+ } else {
+ echo '<p>';
+ }
+ echo $link_item;
+ if ( 'text' == $format ) {
+ echo '</li>';
+ } else {
+ echo '</p>';
+ }
+
+ }
+} // Class Jetpack_RSS_Links_Widget
+
+function jetpack_rss_links_widget_init() {
+ register_widget( 'Jetpack_RSS_Links_Widget' );
+}
+add_action( 'widgets_init', 'jetpack_rss_links_widget_init' );
diff --git a/plugins/jetpack/modules/widgets/search.php b/plugins/jetpack/modules/widgets/search.php
new file mode 100644
index 00000000..54d866b5
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/search.php
@@ -0,0 +1,815 @@
+<?php
+/**
+ * Jetpack Search: Jetpack_Search_Widget class
+ *
+ * @package Jetpack
+ * @subpackage Jetpack Search
+ * @since 5.0.0
+ */
+
+add_action( 'widgets_init', 'jetpack_search_widget_init' );
+
+function jetpack_search_widget_init() {
+ if ( ! Jetpack::is_active() || ! Jetpack_Plan::supports( 'search' ) ) {
+ return;
+ }
+
+ require_once JETPACK__PLUGIN_DIR . 'modules/search/class.jetpack-search-helpers.php';
+
+ register_widget( 'Jetpack_Search_Widget' );
+}
+
+/**
+ * Provides a widget to show available/selected filters on searches.
+ *
+ * @since 5.0.0
+ *
+ * @see WP_Widget
+ */
+class Jetpack_Search_Widget extends WP_Widget {
+
+ /**
+ * The Jetpack_Search instance.
+ *
+ * @since 5.7.0
+ * @var Jetpack_Search
+ */
+ protected $jetpack_search;
+
+ /**
+ * Number of aggregations (filters) to show by default.
+ *
+ * @since 5.8.0
+ * @var int
+ */
+ const DEFAULT_FILTER_COUNT = 5;
+
+ /**
+ * Default sort order for search results.
+ *
+ * @since 5.8.0
+ * @var string
+ */
+ const DEFAULT_SORT = 'relevance_desc';
+
+ /**
+ * Jetpack_Search_Widget constructor.
+ *
+ * @since 5.0.0
+ */
+ public function __construct( $name = null ) {
+ if ( empty( $name ) ) {
+ $name = esc_html__( 'Search', 'jetpack' );
+ }
+ parent::__construct(
+ Jetpack_Search_Helpers::FILTER_WIDGET_BASE,
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', $name ),
+ array(
+ 'classname' => 'jetpack-filters widget_search',
+ 'description' => __( 'Replaces the default search with an Elasticsearch-powered search interface and filters.', 'jetpack' ),
+ )
+ );
+
+ if (
+ Jetpack_Search_Helpers::is_active_widget( $this->id ) &&
+ ! $this->is_search_active()
+ ) {
+ $this->activate_search();
+ }
+
+ if ( is_admin() ) {
+ add_action( 'sidebar_admin_setup', array( $this, 'widget_admin_setup' ) );
+ } else {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) );
+ }
+
+ add_action( 'jetpack_search_render_filters_widget_title', array( 'Jetpack_Search_Template_Tags', 'render_widget_title' ), 10, 3 );
+ add_action( 'jetpack_search_render_filters', array( 'Jetpack_Search_Template_Tags', 'render_available_filters' ), 10, 2 );
+ }
+
+ /**
+ * Check whether search is currently active
+ *
+ * @since 6.3
+ */
+ public function is_search_active() {
+ return Jetpack::is_module_active( 'search' );
+ }
+
+ /**
+ * Activate search
+ *
+ * @since 6.3
+ */
+ public function activate_search() {
+ Jetpack::activate_module( 'search', false, false );
+ }
+
+
+ /**
+ * Enqueues the scripts and styles needed for the customizer.
+ *
+ * @since 5.7.0
+ */
+ public function widget_admin_setup() {
+ wp_enqueue_style( 'widget-jetpack-search-filters', plugins_url( 'search/css/search-widget-admin-ui.css', __FILE__ ) );
+
+ // Required for Tracks
+ wp_register_script(
+ 'jp-tracks',
+ '//stats.wp.com/w.js',
+ array(),
+ gmdate( 'YW' ),
+ true
+ );
+
+ wp_register_script(
+ 'jp-tracks-functions',
+ plugins_url( '_inc/lib/tracks/tracks-callables.js', JETPACK__PLUGIN_FILE ),
+ array(),
+ JETPACK__VERSION,
+ false
+ );
+
+ wp_register_script(
+ 'jetpack-search-widget-admin',
+ plugins_url( 'search/js/search-widget-admin.js', __FILE__ ),
+ array( 'jquery', 'jquery-ui-sortable', 'jp-tracks', 'jp-tracks-functions' ),
+ JETPACK__VERSION
+ );
+
+ wp_localize_script(
+ 'jetpack-search-widget-admin', 'jetpack_search_filter_admin', array(
+ 'defaultFilterCount' => self::DEFAULT_FILTER_COUNT,
+ 'tracksUserData' => Jetpack_Tracks_Client::get_connected_user_tracks_identity(),
+ 'tracksEventData' => array(
+ 'is_customizer' => (int) is_customize_preview(),
+ ),
+ 'i18n' => array(
+ 'month' => Jetpack_Search_Helpers::get_date_filter_type_name( 'month', false ),
+ 'year' => Jetpack_Search_Helpers::get_date_filter_type_name( 'year', false ),
+ 'monthUpdated' => Jetpack_Search_Helpers::get_date_filter_type_name( 'month', true ),
+ 'yearUpdated' => Jetpack_Search_Helpers::get_date_filter_type_name( 'year', true ),
+ ),
+ )
+ );
+
+ wp_enqueue_script( 'jetpack-search-widget-admin' );
+ }
+
+ /**
+ * Enqueue scripts and styles for the frontend.
+ *
+ * @since 5.8.0
+ */
+ public function enqueue_frontend_scripts() {
+ if ( ! is_active_widget( false, false, $this->id_base, true ) ) {
+ return;
+ }
+
+ wp_enqueue_script(
+ 'jetpack-search-widget',
+ plugins_url( 'search/js/search-widget.js', __FILE__ ),
+ array( 'jquery' ),
+ JETPACK__VERSION,
+ true
+ );
+
+ wp_enqueue_style( 'jetpack-search-widget', plugins_url( 'search/css/search-widget-frontend.css', __FILE__ ) );
+ }
+
+ /**
+ * Get the list of valid sort types/orders.
+ *
+ * @since 5.8.0
+ *
+ * @return array The sort orders.
+ */
+ private function get_sort_types() {
+ return array(
+ 'relevance|DESC' => is_admin() ? esc_html__( 'Relevance (recommended)', 'jetpack' ) : esc_html__( 'Relevance', 'jetpack' ),
+ 'date|DESC' => esc_html__( 'Newest first', 'jetpack' ),
+ 'date|ASC' => esc_html__( 'Oldest first', 'jetpack' ),
+ );
+ }
+
+ /**
+ * Callback for an array_filter() call in order to only get filters for the current widget.
+ *
+ * @see Jetpack_Search_Widget::widget()
+ *
+ * @since 5.7.0
+ *
+ * @param array $item Filter item.
+ *
+ * @return bool Whether the current filter item is for the current widget.
+ */
+ function is_for_current_widget( $item ) {
+ return isset( $item['widget_id'] ) && $this->id == $item['widget_id'];
+ }
+
+ /**
+ * This method returns a boolean for whether the widget should show site-wide filters for the site.
+ *
+ * This is meant to provide backwards-compatibility for VIP, and other professional plan users, that manually
+ * configured filters via `Jetpack_Search::set_filters()`.
+ *
+ * @since 5.7.0
+ *
+ * @return bool Whether the widget should display site-wide filters or not.
+ */
+ public function should_display_sitewide_filters() {
+ $filter_widgets = get_option( 'widget_jetpack-search-filters' );
+
+ // This shouldn't be empty, but just for sanity
+ if ( empty( $filter_widgets ) ) {
+ return false;
+ }
+
+ // If any widget has any filters, return false
+ foreach ( $filter_widgets as $number => $widget ) {
+ $widget_id = sprintf( '%s-%d', $this->id_base, $number );
+ if ( ! empty( $widget['filters'] ) && is_active_widget( false, $widget_id, $this->id_base ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function jetpack_search_populate_defaults( $instance ) {
+ $instance = wp_parse_args(
+ (array) $instance, array(
+ 'title' => '',
+ 'search_box_enabled' => true,
+ 'user_sort_enabled' => true,
+ 'sort' => self::DEFAULT_SORT,
+ 'filters' => array( array() ),
+ 'post_types' => array(),
+ )
+ );
+
+ return $instance;
+ }
+
+ /**
+ * Responsible for rendering the widget on the frontend.
+ *
+ * @since 5.0.0
+ *
+ * @param array $args Widgets args supplied by the theme.
+ * @param array $instance The current widget instance.
+ */
+ public function widget( $args, $instance ) {
+ $instance = $this->jetpack_search_populate_defaults( $instance );
+
+ $display_filters = false;
+
+ if ( Jetpack::is_development_mode() ) {
+ echo $args['before_widget'];
+ ?><div id="<?php echo esc_attr( $this->id ); ?>-wrapper">
+ <div class="jetpack-search-sort-wrapper">
+ <label>
+ <?php esc_html_e( 'Jetpack Search not supported in Development Mode', 'jetpack' ); ?>
+ </label>
+ </div>
+ </div><?php
+ echo $args['after_widget'];
+ return;
+ }
+
+ if ( is_search() ) {
+ if ( Jetpack_Search_Helpers::should_rerun_search_in_customizer_preview() ) {
+ Jetpack_Search::instance()->update_search_results_aggregations();
+ }
+
+ $filters = Jetpack_Search::instance()->get_filters();
+
+ if ( ! Jetpack_Search_Helpers::are_filters_by_widget_disabled() && ! $this->should_display_sitewide_filters() ) {
+ $filters = array_filter( $filters, array( $this, 'is_for_current_widget' ) );
+ }
+
+ if ( ! empty( $filters ) ) {
+ $display_filters = true;
+ }
+ }
+
+ if ( ! $display_filters && empty( $instance['search_box_enabled'] ) && empty( $instance['user_sort_enabled'] ) ) {
+ return;
+ }
+
+ $title = isset( $instance['title'] ) ? $instance['title'] : '';
+
+ if ( empty( $title ) ) {
+ $title = '';
+ }
+
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
+
+ echo $args['before_widget'];
+ ?><div id="<?php echo esc_attr( $this->id ); ?>-wrapper">
+ <?php
+
+ if ( ! empty( $title ) ) {
+ /**
+ * Responsible for displaying the title of the Jetpack Search filters widget.
+ *
+ * @module search
+ *
+ * @since 5.7.0
+ *
+ * @param string $title The widget's title
+ * @param string $args['before_title'] The HTML tag to display before the title
+ * @param string $args['after_title'] The HTML tag to display after the title
+ */
+ do_action( 'jetpack_search_render_filters_widget_title', $title, $args['before_title'], $args['after_title'] );
+ }
+
+ $default_sort = isset( $instance['sort'] ) ? $instance['sort'] : self::DEFAULT_SORT;
+ list( $orderby, $order ) = $this->sorting_to_wp_query_param( $default_sort );
+ $current_sort = "{$orderby}|{$order}";
+
+ // we need to dynamically inject the sort field into the search box when the search box is enabled, and display
+ // it separately when it's not.
+ if ( ! empty( $instance['search_box_enabled'] ) ) {
+ Jetpack_Search_Template_Tags::render_widget_search_form( $instance['post_types'], $orderby, $order );
+ }
+
+ if ( ! empty( $instance['search_box_enabled'] ) && ! empty( $instance['user_sort_enabled'] ) ) :
+ ?>
+ <div class="jetpack-search-sort-wrapper">
+ <label>
+ <?php esc_html_e( 'Sort by', 'jetpack' ); ?>
+ <select class="jetpack-search-sort">
+ <?php foreach ( $this->get_sort_types() as $sort => $label ) { ?>
+ <option value="<?php echo esc_attr( $sort ); ?>" <?php selected( $current_sort, $sort ); ?>>
+ <?php echo esc_html( $label ); ?>
+ </option>
+ <?php } ?>
+ </select>
+ </label>
+ </div>
+ <?php
+ endif;
+
+ if ( $display_filters ) {
+ /**
+ * Responsible for rendering filters to narrow down search results.
+ *
+ * @module search
+ *
+ * @since 5.8.0
+ *
+ * @param array $filters The possible filters for the current query.
+ * @param array $post_types An array of post types to limit filtering to.
+ */
+ do_action(
+ 'jetpack_search_render_filters',
+ $filters,
+ isset( $instance['post_types'] ) ? $instance['post_types'] : null
+ );
+ }
+
+ $this->maybe_render_sort_javascript( $instance, $order, $orderby );
+
+ echo '</div>';
+ echo $args['after_widget'];
+ }
+
+ /**
+ * Renders JavaScript for the sorting controls on the frontend.
+ *
+ * This JS is a bit complicated, but here's what it's trying to do:
+ * - find the search form
+ * - find the orderby/order fields and set default values
+ * - detect changes to the sort field, if it exists, and use it to set the order field values
+ *
+ * @since 5.8.0
+ *
+ * @param array $instance The current widget instance.
+ * @param string $order The order to initialize the select with.
+ * @param string $orderby The orderby to initialize the select with.
+ */
+ private function maybe_render_sort_javascript( $instance, $order, $orderby ) {
+ if ( ! empty( $instance['user_sort_enabled'] ) ) :
+ ?>
+ <script type="text/javascript">
+ jQuery( document ).ready( function( $ ) {
+ var orderByDefault = '<?php echo 'date' === $orderby ? 'date' : 'relevance'; ?>',
+ orderDefault = '<?php echo 'ASC' === $order ? 'ASC' : 'DESC'; ?>',
+ widgetId = decodeURIComponent( '<?php echo rawurlencode( $this->id ); ?>' ),
+ searchQuery = decodeURIComponent( '<?php echo rawurlencode( get_query_var( 's', '' ) ); ?>' ),
+ isSearch = <?php echo (int) is_search(); ?>;
+
+ var container = $( '#' + widgetId + '-wrapper' ),
+ form = container.find('.jetpack-search-form form'),
+ orderBy = form.find( 'input[name=orderby]'),
+ order = form.find( 'input[name=order]'),
+ searchInput = form.find( 'input[name="s"]' );
+
+ orderBy.val( orderByDefault );
+ order.val( orderDefault );
+
+ // Some themes don't set the search query, which results in the query being lost
+ // when doing a sort selection. So, if the query isn't set, let's set it now. This approach
+ // is chosen over running a regex over HTML for every search query performed.
+ if ( isSearch && ! searchInput.val() ) {
+ searchInput.val( searchQuery );
+ }
+
+ searchInput.addClass( 'show-placeholder' );
+
+ container.find( '.jetpack-search-sort' ).change( function( event ) {
+ var values = event.target.value.split( '|' );
+ orderBy.val( values[0] );
+ order.val( values[1] );
+
+ form.submit();
+ });
+ } );
+ </script>
+ <?php
+ endif;
+ }
+
+ /**
+ * Convert a sort string into the separate order by and order parts.
+ *
+ * @since 5.8.0
+ *
+ * @param string $sort A sort string.
+ *
+ * @return array Order by and order.
+ */
+ private function sorting_to_wp_query_param( $sort ) {
+ $parts = explode( '|', $sort );
+ $orderby = isset( $_GET['orderby'] )
+ ? $_GET['orderby']
+ : $parts[0];
+
+ $order = isset( $_GET['order'] )
+ ? strtoupper( $_GET['order'] )
+ : ( ( isset( $parts[1] ) && 'ASC' === strtoupper( $parts[1] ) ) ? 'ASC' : 'DESC' );
+
+ return array( $orderby, $order );
+ }
+
+ /**
+ * Updates a particular instance of the widget. Validates and sanitizes the options.
+ *
+ * @since 5.0.0
+ *
+ * @param array $new_instance New settings for this instance as input by the user via Jetpack_Search_Widget::form().
+ * @param array $old_instance Old settings for this instance.
+ *
+ * @return array Settings to save.
+ */
+ public function update( $new_instance, $old_instance ) {
+ $instance = array();
+
+ $instance['title'] = sanitize_text_field( $new_instance['title'] );
+ $instance['search_box_enabled'] = empty( $new_instance['search_box_enabled'] ) ? '0' : '1';
+ $instance['user_sort_enabled'] = empty( $new_instance['user_sort_enabled'] ) ? '0' : '1';
+ $instance['sort'] = $new_instance['sort'];
+ $instance['post_types'] = empty( $new_instance['post_types'] ) || empty( $instance['search_box_enabled'] )
+ ? array()
+ : array_map( 'sanitize_key', $new_instance['post_types'] );
+
+ $filters = array();
+ if ( isset( $new_instance['filter_type'] ) ) {
+ foreach ( (array) $new_instance['filter_type'] as $index => $type ) {
+ $count = intval( $new_instance['num_filters'][ $index ] );
+ $count = min( 50, $count ); // Set max boundary at 50.
+ $count = max( 1, $count ); // Set min boundary at 1.
+
+ switch ( $type ) {
+ case 'taxonomy':
+ $filters[] = array(
+ 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ),
+ 'type' => 'taxonomy',
+ 'taxonomy' => sanitize_key( $new_instance['taxonomy_type'][ $index ] ),
+ 'count' => $count,
+ );
+ break;
+ case 'post_type':
+ $filters[] = array(
+ 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ),
+ 'type' => 'post_type',
+ 'count' => $count,
+ );
+ break;
+ case 'date_histogram':
+ $filters[] = array(
+ 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ),
+ 'type' => 'date_histogram',
+ 'count' => $count,
+ 'field' => sanitize_key( $new_instance['date_histogram_field'][ $index ] ),
+ 'interval' => sanitize_key( $new_instance['date_histogram_interval'][ $index ] ),
+ );
+ break;
+ }
+ }
+ }
+
+ if ( ! empty( $filters ) ) {
+ $instance['filters'] = $filters;
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Outputs the settings update form.
+ *
+ * @since 5.0.0
+ *
+ * @param array $instance Current settings.
+ */
+ public function form( $instance ) {
+ $instance = $this->jetpack_search_populate_defaults( $instance );
+
+ $title = strip_tags( $instance['title'] );
+
+ $hide_filters = Jetpack_Search_Helpers::are_filters_by_widget_disabled();
+
+ $classes = sprintf(
+ 'jetpack-search-filters-widget %s %s %s',
+ $hide_filters ? 'hide-filters' : '',
+ $instance['search_box_enabled'] ? '' : 'hide-post-types',
+ $this->id
+ );
+ ?>
+ <div class="<?php echo esc_attr( $classes ); ?>">
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
+ <?php esc_html_e( 'Title (optional):', 'jetpack' ); ?>
+ </label>
+ <input
+ class="widefat"
+ id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $title ); ?>"
+ />
+ </p>
+
+ <p>
+ <label>
+ <input
+ type="checkbox"
+ class="jetpack-search-filters-widget__search-box-enabled"
+ name="<?php echo esc_attr( $this->get_field_name( 'search_box_enabled' ) ); ?>"
+ <?php checked( $instance['search_box_enabled'] ); ?>
+ />
+ <?php esc_html_e( 'Show search box', 'jetpack' ); ?>
+ </label>
+ </p>
+ <p>
+ <label>
+ <input
+ type="checkbox"
+ class="jetpack-search-filters-widget__sort-controls-enabled"
+ name="<?php echo esc_attr( $this->get_field_name( 'user_sort_enabled' ) ); ?>"
+ <?php checked( $instance['user_sort_enabled'] ); ?>
+ <?php disabled( ! $instance['search_box_enabled'] ); ?>
+ />
+ <?php esc_html_e( 'Show sort selection dropdown', 'jetpack' ); ?>
+ </label>
+ </p>
+
+ <p class="jetpack-search-filters-widget__post-types-select">
+ <label><?php esc_html_e( 'Post types to search (minimum of 1):', 'jetpack' ); ?></label>
+ <?php foreach ( get_post_types( array( 'exclude_from_search' => false ), 'objects' ) as $post_type ) : ?>
+ <label>
+ <input
+ type="checkbox"
+ value="<?php echo esc_attr( $post_type->name ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'post_types' ) ); ?>[]"
+ <?php checked( empty( $instance['post_types'] ) || in_array( $post_type->name, $instance['post_types'] ) ); ?>
+ />&nbsp;
+ <?php echo esc_html( $post_type->label ); ?>
+ </label>
+ <?php endforeach; ?>
+ </p>
+
+ <p>
+ <label>
+ <?php esc_html_e( 'Default sort order:', 'jetpack' ); ?>
+ <select
+ name="<?php echo esc_attr( $this->get_field_name( 'sort' ) ); ?>"
+ class="widefat jetpack-search-filters-widget__sort-order">
+ <?php foreach ( $this->get_sort_types() as $sort_type => $label ) { ?>
+ <option value="<?php echo esc_attr( $sort_type ); ?>" <?php selected( $instance['sort'], $sort_type ); ?>>
+ <?php echo esc_html( $label ); ?>
+ </option>
+ <?php } ?>
+ </select>
+ </label>
+ </p>
+
+ <?php if ( ! $hide_filters ) : ?>
+ <script class="jetpack-search-filters-widget__filter-template" type="text/template">
+ <?php echo $this->render_widget_edit_filter( array(), true ); ?>
+ </script>
+ <div class="jetpack-search-filters-widget__filters">
+ <?php foreach ( (array) $instance['filters'] as $filter ) : ?>
+ <?php $this->render_widget_edit_filter( $filter ); ?>
+ <?php endforeach; ?>
+ </div>
+ <p class="jetpack-search-filters-widget__add-filter-wrapper">
+ <a class="button jetpack-search-filters-widget__add-filter" href="#">
+ <?php esc_html_e( 'Add a filter', 'jetpack' ); ?>
+ </a>
+ </p>
+ <noscript>
+ <p class="jetpack-search-filters-help">
+ <?php echo esc_html_e( 'Adding filters requires JavaScript!', 'jetpack' ); ?>
+ </p>
+ </noscript>
+ <?php if ( is_customize_preview() ) : ?>
+ <p class="jetpack-search-filters-help">
+ <a href="https://jetpack.com/support/search/#filters-not-showing-up" target="_blank">
+ <?php esc_html_e( "Why aren't my filters appearing?", 'jetpack' ); ?>
+ </a>
+ </p>
+ <?php endif; ?>
+ <?php endif; ?>
+ </div>
+ <?php
+ }
+
+ /**
+ * We need to render HTML in two formats: an Underscore template (client-side)
+ * and native PHP (server-side). This helper function allows for easy rendering
+ * of attributes in both formats.
+ *
+ * @since 5.8.0
+ *
+ * @param string $name Attribute name.
+ * @param string $value Attribute value.
+ * @param bool $is_template Whether this is for an Underscore template or not.
+ */
+ private function render_widget_attr( $name, $value, $is_template ) {
+ echo $is_template ? "<%= $name %>" : esc_attr( $value );
+ }
+
+ /**
+ * We need to render HTML in two formats: an Underscore template (client-size)
+ * and native PHP (server-side). This helper function allows for easy rendering
+ * of the "selected" attribute in both formats.
+ *
+ * @since 5.8.0
+ *
+ * @param string $name Attribute name.
+ * @param string $value Attribute value.
+ * @param string $compare Value to compare to the attribute value to decide if it should be selected.
+ * @param bool $is_template Whether this is for an Underscore template or not.
+ */
+ private function render_widget_option_selected( $name, $value, $compare, $is_template ) {
+ $compare_js = rawurlencode( $compare );
+ echo $is_template ? "<%= decodeURIComponent( '$compare_js' ) === $name ? 'selected=\"selected\"' : '' %>" : selected( $value, $compare );
+ }
+
+ /**
+ * Responsible for rendering a single filter in the customizer or the widget administration screen in wp-admin.
+ *
+ * We use this method for two purposes - rendering the fields server-side, and also rendering a script template for Underscore.
+ *
+ * @since 5.7.0
+ *
+ * @param array $filter The filter to render.
+ * @param bool $is_template Whether this is for an Underscore template or not.
+ */
+ public function render_widget_edit_filter( $filter, $is_template = false ) {
+ $args = wp_parse_args(
+ $filter, array(
+ 'name' => '',
+ 'type' => 'taxonomy',
+ 'taxonomy' => '',
+ 'post_type' => '',
+ 'field' => '',
+ 'interval' => '',
+ 'count' => self::DEFAULT_FILTER_COUNT,
+ )
+ );
+
+ $args['name_placeholder'] = Jetpack_Search_Helpers::generate_widget_filter_name( $args );
+
+ ?>
+ <div class="jetpack-search-filters-widget__filter is-<?php $this->render_widget_attr( 'type', $args['type'], $is_template ); ?>">
+ <p class="jetpack-search-filters-widget__type-select">
+ <label>
+ <?php esc_html_e( 'Filter Type:', 'jetpack' ); ?>
+ <select name="<?php echo esc_attr( $this->get_field_name( 'filter_type' ) ); ?>[]" class="widefat filter-select">
+ <option value="taxonomy" <?php $this->render_widget_option_selected( 'type', $args['type'], 'taxonomy', $is_template ); ?>>
+ <?php esc_html_e( 'Taxonomy', 'jetpack' ); ?>
+ </option>
+ <option value="post_type" <?php $this->render_widget_option_selected( 'type', $args['type'], 'post_type', $is_template ); ?>>
+ <?php esc_html_e( 'Post Type', 'jetpack' ); ?>
+ </option>
+ <option value="date_histogram" <?php $this->render_widget_option_selected( 'type', $args['type'], 'date_histogram', $is_template ); ?>>
+ <?php esc_html_e( 'Date', 'jetpack' ); ?>
+ </option>
+ </select>
+ </label>
+ </p>
+
+ <p class="jetpack-search-filters-widget__taxonomy-select">
+ <label>
+ <?php
+ esc_html_e( 'Choose a taxonomy:', 'jetpack' );
+ $seen_taxonomy_labels = array();
+ ?>
+ <select name="<?php echo esc_attr( $this->get_field_name( 'taxonomy_type' ) ); ?>[]" class="widefat taxonomy-select">
+ <?php foreach ( get_taxonomies( array( 'public' => true ), 'objects' ) as $taxonomy ) : ?>
+ <option value="<?php echo esc_attr( $taxonomy->name ); ?>" <?php $this->render_widget_option_selected( 'taxonomy', $args['taxonomy'], $taxonomy->name, $is_template ); ?>>
+ <?php
+ $label = in_array( $taxonomy->label, $seen_taxonomy_labels )
+ ? sprintf(
+ /* translators: %1$s is the taxonomy name, %2s is the name of its type to help distinguish between several taxonomies with the same name, e.g. category and tag. */
+ _x( '%1$s (%2$s)', 'A label for a taxonomy selector option', 'jetpack' ),
+ $taxonomy->label,
+ $taxonomy->name
+ )
+ : $taxonomy->label;
+ echo esc_html( $label );
+ $seen_taxonomy_labels[] = $taxonomy->label;
+ ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </label>
+ </p>
+
+ <p class="jetpack-search-filters-widget__date-histogram-select">
+ <label>
+ <?php esc_html_e( 'Choose a field:', 'jetpack' ); ?>
+ <select name="<?php echo esc_attr( $this->get_field_name( 'date_histogram_field' ) ); ?>[]" class="widefat date-field-select">
+ <option value="post_date" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_date', $is_template ); ?>>
+ <?php esc_html_e( 'Date', 'jetpack' ); ?>
+ </option>
+ <option value="post_date_gmt" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_date_gmt', $is_template ); ?>>
+ <?php esc_html_e( 'Date GMT', 'jetpack' ); ?>
+ </option>
+ <option value="post_modified" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_modified', $is_template ); ?>>
+ <?php esc_html_e( 'Modified', 'jetpack' ); ?>
+ </option>
+ <option value="post_modified_gmt" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_modified_gmt', $is_template ); ?>>
+ <?php esc_html_e( 'Modified GMT', 'jetpack' ); ?>
+ </option>
+ </select>
+ </label>
+ </p>
+
+ <p class="jetpack-search-filters-widget__date-histogram-select">
+ <label>
+ <?php esc_html_e( 'Choose an interval:' ); ?>
+ <select name="<?php echo esc_attr( $this->get_field_name( 'date_histogram_interval' ) ); ?>[]" class="widefat date-interval-select">
+ <option value="month" <?php $this->render_widget_option_selected( 'interval', $args['interval'], 'month', $is_template ); ?>>
+ <?php esc_html_e( 'Month', 'jetpack' ); ?>
+ </option>
+ <option value="year" <?php $this->render_widget_option_selected( 'interval', $args['interval'], 'year', $is_template ); ?>>
+ <?php esc_html_e( 'Year', 'jetpack' ); ?>
+ </option>
+ </select>
+ </label>
+ </p>
+
+ <p class="jetpack-search-filters-widget__title">
+ <label>
+ <?php esc_html_e( 'Title:', 'jetpack' ); ?>
+ <input
+ class="widefat"
+ type="text"
+ name="<?php echo esc_attr( $this->get_field_name( 'filter_name' ) ); ?>[]"
+ value="<?php $this->render_widget_attr( 'name', $args['name'], $is_template ); ?>"
+ placeholder="<?php $this->render_widget_attr( 'name_placeholder', $args['name_placeholder'], $is_template ); ?>"
+ />
+ </label>
+ </p>
+
+ <p>
+ <label>
+ <?php esc_html_e( 'Maximum number of filters (1-50):', 'jetpack' ); ?>
+ <input
+ class="widefat filter-count"
+ name="<?php echo esc_attr( $this->get_field_name( 'num_filters' ) ); ?>[]"
+ type="number"
+ value="<?php $this->render_widget_attr( 'count', $args['count'], $is_template ); ?>"
+ min="1"
+ max="50"
+ step="1"
+ required
+ />
+ </label>
+ </p>
+
+ <p class="jetpack-search-filters-widget__controls">
+ <a href="#" class="delete"><?php esc_html_e( 'Remove', 'jetpack' ); ?></a>
+ </p>
+ </div>
+ <?php
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/search/css/search-widget-admin-ui.css b/plugins/jetpack/modules/widgets/search/css/search-widget-admin-ui.css
new file mode 100644
index 00000000..a3313f05
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/search/css/search-widget-admin-ui.css
@@ -0,0 +1,87 @@
+.jetpack-search-filters-widget__filter {
+ background: #f9f9f9;
+ border: 1px solid #dfdfdf;
+ padding: 0 12px;
+ margin-bottom: 12px;
+ cursor: move;
+}
+
+.jetpack-search-filters-widget__controls {
+ text-align: right;
+}
+
+.jetpack-search-filters-widget .jetpack-search-filters-widget__sort-controls-enabled {
+ margin-left: 24px;
+}
+
+.jetpack-search-filters-widget__controls .delete {
+ color: #a00;
+}
+
+.jetpack-search-filters-widget.hide-filters .jetpack-search-filters-widget__filter {
+ display: none;
+}
+
+.button.jetpack-search-filters-widget__add-filter {
+ margin-bottom: 10px;
+}
+
+/* Assume that taxonomy select is the default selected. Other controls should be hidden here. */
+.jetpack-search-filters-widget__post-type-select {
+ display: none;
+}
+
+.jetpack-search-filters-widget__date-histogram-select {
+ display: none;
+}
+
+.jetpack-search-filters-widget__filter-placeholder {
+ border: 1px #555 dashed;
+ background-color: #eee;
+ height: 286px;
+ margin-bottom: 12px;
+}
+
+/* When post type is selected, remove the other controls */
+.jetpack-search-filters-widget__filter.is-post_type .jetpack-search-filters-widget__taxonomy-select {
+ display: none;
+}
+
+/* When date is selected, remove the other controls */
+.jetpack-search-filters-widget__filter.is-date_histogram .jetpack-search-filters-widget__date-histogram-select {
+ display: inline;
+}
+
+.jetpack-search-filters-widget__filter.is-date_histogram .jetpack-search-filters-widget__taxonomy-select {
+ display: none;
+}
+
+.jetpack-search-filters-widget.hide-post-types .jetpack-search-filters-widget__post-types-select {
+ display: none;
+}
+
+.jetpack-search-filters-help:before {
+ display: inline-block;
+ position: relative;
+ font-family: dashicons;
+ font-size: 20px;
+ top: 5px;
+ line-height: 1px;
+ content:"\f223";
+}
+.jetpack-search-filters-help {
+ padding: 5px 5px 15px 0;
+}
+
+.jetpack-search-filters-widget__post-types-select label {
+ display: block;
+ margin-bottom: 4px;
+}
+
+.jetpack-search-filters-widget__post-types-select input[type="checkbox"] {
+ margin-left: 24px;
+}
+
+body.no-js .jetpack-search-filters-widget__add-filter-wrapper {
+ display: none;
+}
diff --git a/plugins/jetpack/modules/widgets/search/css/search-widget-frontend.css b/plugins/jetpack/modules/widgets/search/css/search-widget-frontend.css
new file mode 100644
index 00000000..58c7cf3e
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/search/css/search-widget-frontend.css
@@ -0,0 +1,66 @@
+.jetpack-search-filters-widget__sub-heading {
+ font-size: inherit;
+ font-weight: bold;
+ margin: 0 0 .5em;
+ padding: 0;
+}
+
+/* The first heading after the form */
+.jetpack-search-form + .jetpack-search-filters-widget__sub-heading {
+ margin-top: 1.5em;
+ margin-bottom: 0.5em !important;
+}
+
+.jetpack-search-filters-widget__clear {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.jetpack-search-sort-wrapper {
+ margin-top: 1em;
+ margin-bottom: 1.5em;
+}
+
+.jetpack-search-sort-wrapper label {
+ display: inherit;
+}
+
+.widget_search .jetpack-search-filters-widget__filter-list input[type="checkbox"] {
+ width: auto;
+ height: auto;
+}
+
+ul.jetpack-search-filters-widget__filter-list li {
+ border: none;
+ padding: 0;
+ list-style: none;
+}
+
+ul.jetpack-search-filters-widget__filter-list li a {
+ text-decoration: none;
+}
+
+ul.jetpack-search-filters-widget__filter-list li a:hover {
+ box-shadow: none;
+}
+
+ul.jetpack-search-filters-widget__filter-list li label {
+ font-weight: inherit;
+ display: inherit;
+}
+
+.jetpack-search-filters-widget__filter-list {
+ list-style: none;
+}
+
+ul.jetpack-search-filters-widget__filter-list {
+ margin-bottom: 1.5em;
+}
+
+body.search .jetpack-search-form input[name="s"]::placeholder {
+ color: transparent;
+}
+
+body.search .jetpack-search-form input[name="s"].show-placeholder::placeholder {
+ color: inherit;
+}
diff --git a/plugins/jetpack/modules/widgets/search/js/search-widget-admin.js b/plugins/jetpack/modules/widgets/search/js/search-widget-admin.js
new file mode 100644
index 00000000..5840a408
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/search/js/search-widget-admin.js
@@ -0,0 +1,360 @@
+/* globals jetpack_search_filter_admin, jQuery, analytics */
+
+( function( $, args ) {
+ var defaultFilterCount = ( 'undefined' !== typeof args && args.defaultFilterCount ) ?
+ args.defaultFilterCount :
+ 5; // Just in case we couldn't find the defaultFiltercount arg
+
+ $( document ).ready( function() {
+ setListeners();
+
+ window.JetpackSearch = window.JetpackSearch || {};
+ window.JetpackSearch.addFilter = addFilter;
+
+ // Initialize Tracks
+ if ( 'undefined' !== typeof analytics && args.tracksUserData ) {
+ analytics.initialize( args.tracksUserData.userid, args.tracksUserData.username );
+ }
+ } );
+
+ function generateFilterTitlePlaceholder( container ) {
+ var placeholder = null,
+ isModified = null,
+ isMonth = null,
+ type = container.find( '.filter-select' ).val();
+
+ if ( 'taxonomy' === type ) {
+ placeholder = container.find('.taxonomy-select option:selected').text().trim();
+ } else if ( 'date_histogram' === type && args && args.i18n ) {
+ isModified = ( -1 !== container.find( '.date-field-select' ).val().indexOf( 'modified' ) );
+ isMonth = ( 'month' === container.find( '.date-interval-select' ).val() );
+
+ if ( isMonth ) {
+ placeholder = isModified ?
+ args.i18n.monthUpdated :
+ args.i18n.month;
+ } else {
+ placeholder = isModified ?
+ args.i18n.yearUpdated :
+ args.i18n.year;
+ }
+ } else {
+ placeholder = container.find('.filter-select option:selected').text().trim();
+ }
+
+ $( container ).find('.jetpack-search-filters-widget__title input').prop( 'placeholder', placeholder );
+ }
+
+ var addFilter = function( filtersContainer, args ) {
+ var template = _.template(
+ filtersContainer
+ .closest( '.jetpack-search-filters-widget' )
+ .find( '.jetpack-search-filters-widget__filter-template' )
+ .html()
+ );
+ generateFilterTitlePlaceholder( filtersContainer.append( template( args ) ) );
+ };
+
+ var setListeners = function( widget ) {
+ widget = ( 'undefined' === typeof widget ) ?
+ $( '.jetpack-search-filters-widget' ):
+ widget;
+
+ var getContainer = function( el ) {
+ return $( el ).closest('.jetpack-search-filters-widget__filter');
+ };
+
+ widget.on( 'change', '.filter-select', function() {
+ var select = $( this ),
+ selectVal = select.val(),
+ eventArgs = {
+ is_customizer: args.tracksEventData.is_customizer
+ };
+
+ eventArgs.type = selectVal;
+
+ select
+ .closest( '.jetpack-search-filters-widget__filter' )
+ .attr( 'class', 'jetpack-search-filters-widget__filter' )
+ .addClass( 'is-' + selectVal );
+
+ generateFilterTitlePlaceholder( getContainer( this ) );
+
+ trackAndBumpMCStats( 'changed_filter_type', eventArgs );
+ } );
+
+ // enable showing sort controls only if showing search box is enabled
+ widget.on( 'change', '.jetpack-search-filters-widget__search-box-enabled', function() {
+ var checkbox = $( this ),
+ checkboxVal = checkbox.is(':checked'),
+ filterParent = checkbox.closest( '.jetpack-search-filters-widget' ),
+ sortControl = filterParent.find( '.jetpack-search-filters-widget__sort-controls-enabled' );
+
+ filterParent.toggleClass( 'hide-post-types' );
+
+ if ( checkboxVal ) {
+ sortControl.removeAttr( 'disabled' );
+ trackAndBumpMCStats( 'enabled_search_box', args.tracksEventData );
+ } else {
+ sortControl.prop( 'checked', false );
+ sortControl.prop( 'disabled', true );
+ trackAndBumpMCStats( 'disabled_search_box', args.tracksEventData );
+ }
+ } );
+
+ widget.on( 'change', '.jetpack-search-filters-widget__sort-controls-enabled', function() {
+ if ( $( this ).is( ':checked' ) ) {
+ trackAndBumpMCStats( 'enabled_sort_controls', args.tracksEventData );
+ } else {
+ trackAndBumpMCStats( 'disabled_sort_controls', args.tracksEventData );
+ }
+ } );
+
+ widget.on( 'click', '.jetpack-search-filters-widget__post-types-select input[type="checkbox"]', function( e ) {
+ var t = $( this );
+ var siblingsChecked = t.closest( '.jetpack-search-filters-widget' )
+ .find( '.jetpack-search-filters-widget__post-types-select input[type="checkbox"]:checked' );
+
+ if ( 0 === siblingsChecked.length ) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ trackAndBumpMCStats( 'attempted_no_post_types', args.tracksEventData );
+ }
+ } );
+
+ widget.on( 'change', '.jetpack-search-filters-widget__post-types-select input[type="checkbox"]', function() {
+ var t = $( this );
+ var eventArgs = {
+ is_customizer: args.tracksEventData.is_customizer,
+ post_type: t.val()
+ };
+
+ if ( wp && wp.customize ) {
+ wp.customize.state( 'saved' ).set( false );
+ }
+
+ if ( t.is( ':checked' ) ) {
+ trackAndBumpMCStats( 'added_post_type', eventArgs );
+ } else {
+ trackAndBumpMCStats( 'removed_post_type', eventArgs );
+ }
+ } );
+
+ widget.on( 'change', '.jetpack-search-filters-widget__sort-order', function() {
+ var eventArgs = {
+ is_customizer: args.tracksEventData.is_customizer
+ };
+
+ eventArgs.order = $( this ).val();
+
+ if ( wp && wp.customize ) {
+ wp.customize.state( 'saved' ).set( false );
+ }
+
+ trackAndBumpMCStats( 'changed_sort_order', eventArgs );
+ } );
+
+ widget.on( 'change', '.jetpack-search-filters-widget__taxonomy-select select', function() {
+ var eventArgs = {
+ is_customizer: args.tracksEventData.is_customizer
+ };
+
+ eventArgs.taxonomy = $( this ).val();
+
+ generateFilterTitlePlaceholder( getContainer( this ) );
+
+ if ( wp && wp.customize ) {
+ wp.customize.state( 'saved' ).set( false );
+ }
+
+ trackAndBumpMCStats( 'changed_taxonomy', eventArgs );
+ } );
+
+ widget.on( 'change', 'select.date-field-select', function() {
+ var eventArgs = {
+ is_customizer: args.tracksEventData.is_customizer
+ };
+
+ eventArgs.field = $( this ).val();
+
+ generateFilterTitlePlaceholder( getContainer( this ) );
+
+ if ( wp && wp.customize ) {
+ wp.customize.state( 'saved' ).set( false );
+ }
+
+ trackAndBumpMCStats( 'changed_date_field', eventArgs );
+ } );
+
+ widget.on( 'change', 'select.date-interval-select', function() {
+ var eventArgs = {
+ is_customizer: args.tracksEventData.is_customizer
+ };
+
+ eventArgs.interval = $( this ).val();
+
+ generateFilterTitlePlaceholder( getContainer( this ) );
+
+ if ( wp && wp.customize ) {
+ wp.customize.state( 'saved' ).set( false );
+ }
+
+ trackAndBumpMCStats( 'changed_date_interval', eventArgs );
+ } );
+
+ widget.on( 'change', 'input.filter-count', function() {
+ var eventArgs = {
+ is_customizer: args.tracksEventData.is_customizer
+ };
+
+ eventArgs.count = $( this ).val();
+
+ if ( wp && wp.customize ) {
+ wp.customize.state( 'saved' ).set( false );
+ }
+
+ trackAndBumpMCStats( 'changed_filter_count', eventArgs );
+ } );
+
+ // add filter button
+ widget.on( 'click', '.jetpack-search-filters-widget__add-filter', function( e ) {
+ e.preventDefault();
+
+ var filtersContainer = $( this )
+ .closest( '.jetpack-search-filters-widget' )
+ .find( '.jetpack-search-filters-widget__filters' );
+
+ addFilter( filtersContainer, {
+ type: 'taxonomy',
+ taxonomy: '',
+ post_type: '',
+ field: '',
+ interval: '',
+ count: defaultFilterCount,
+ name_placeholder: '',
+ name: ''
+ } );
+
+ if ( wp && wp.customize ) {
+ wp.customize.state( 'saved' ).set( false );
+ }
+
+ // Trigger change event to let legacy widget admin know the widget state is "dirty"
+ filtersContainer
+ .find( '.jetpack-search-filters-widget__filter' )
+ .find( 'input, textarea, select' )
+ .change();
+
+ trackAndBumpMCStats( 'added_filter', args.tracksEventData );
+ } );
+
+ widget.on( 'click', '.jetpack-search-filters-widget__controls .delete', function( e ) {
+ e.preventDefault();
+ var filter = $( this ).closest( '.jetpack-search-filters-widget__filter' ),
+ eventArgs = {
+ is_customizer: args.tracksEventData.is_customizer
+ };
+
+ eventArgs.type = filter.find( '.filter-select' ).val();
+
+ switch ( eventArgs.type ) {
+ case 'taxonomy':
+ eventArgs.taxonomy = filter.find( '.jetpack-search-filters-widget__taxonomy-select select' ).val();
+ break;
+ case 'date_histogram':
+ eventArgs.dateField = filter.find( '.jetpack-search-filters-widget__date-histogram-select:first select' ).val();
+ eventArgs.dateInterval = filter.find( '.jetpack-search-filters-widget__date-histogram-select:nth-child( 2 ) select' ).val();
+ break;
+ }
+
+ eventArgs.filterCount = filter.find( '.filter-count' ).val();
+
+ trackAndBumpMCStats( 'deleted_filter', eventArgs );
+
+ filter.find( 'input, textarea, select' ).change();
+ filter.remove();
+
+ if ( wp && wp.customize ) {
+ wp.customize.state( 'saved' ).set( false );
+ }
+ } );
+
+ // make the filters sortable
+ $( '.jetpack-search-filters-widget__filters' ).sortable( {
+ placeholder: 'jetpack-search-filters-widget__filter-placeholder',
+ axis: 'y',
+ revert: true,
+ cancel: 'input,textarea,button,select,option,.jetpack-search-filters-widget__controls a',
+ change: function() {
+ if ( wp && wp.customize ) {
+ wp.customize.state( 'saved' ).set( false );
+ }
+ },
+ update: function( e, ui ) {
+ $( ui.item ).find( 'input, textarea, select' ).change();
+ }
+ } )
+ .disableSelection();
+ };
+
+ // When widgets are updated, remove and re-add listeners
+ $( document ).on( 'widget-updated widget-added', function( e, widget ) {
+ widget = $( widget );
+
+ var id = widget.attr( 'id' ),
+ isJetpackSearch = ( id && ( -1 !== id.indexOf( 'jetpack-search-filters' ) ) );
+
+ if ( ! isJetpackSearch ) {
+ return;
+ }
+
+ // Intentionally not tracking widget additions and updates here as these events
+ // seem noisy in the customizer. We'll track those via PHP.
+
+ widget.off( 'change', '.filter-select' );
+ widget.off( 'click', '.jetpack-search-filters-widget__controls .delete' );
+ widget.off( 'change', '.jetpack-search-filters-widget__use-filters' );
+ widget.off( 'change', '.jetpack-search-filters-widget__search-box-enabled' );
+ widget.off( 'change', '.jetpack-search-filters-widget__sort-controls-enabled' );
+ widget.off( 'change', '.jetpack-search-filters-widget__sort-controls-enabled' );
+ widget.off( 'change', '.jetpack-search-filters-widget__post-type-selector' );
+ widget.off( 'change', '.jetpack-search-filters-widget__sort-order' );
+ widget.off( 'change', '.jetpack-search-filters-widget__taxonomy-select' );
+ widget.off( 'change', '.jetpack-search-filters-widget__date-histogram-select:first select' );
+ widget.off( 'change', '.jetpack-search-filters-widget__date-histogram-select:eq(1) select' );
+ widget.off( 'click', '.jetpack-search-filters-widget__post-types-select input[type="checkbox"]' );
+ widget.off( 'click', '.jetpack-search-filters-widget__add-filter');
+
+ setListeners( widget );
+ } );
+
+ /**
+ * This function will fire both a Tracks and MC stat.
+ *
+ * Tracks: Will be prefixed by 'jetpack_widget_search_' and use underscores.
+ * MC: Will not be prefixed, and will use dashes.
+ *
+ * Logic borrowed from `idc-notice.js`.
+ *
+ * @param eventName string
+ * @param extraProps object
+ */
+ function trackAndBumpMCStats( eventName, extraProps ) {
+ if ( 'undefined' === typeof extraProps || 'object' !== typeof extraProps ) {
+ extraProps = {};
+ }
+
+ if ( eventName && eventName.length && 'undefined' !== typeof analytics && analytics.tracks && analytics.mc ) {
+ // Format for Tracks
+ eventName = eventName.replace( /-/g, '_' );
+ eventName = eventName.indexOf( 'jetpack_widget_search_' ) !== 0 ? 'jetpack_widget_search_' + eventName : eventName;
+ analytics.tracks.recordEvent( eventName, extraProps );
+
+ // Now format for MC stats
+ eventName = eventName.replace( 'jetpack_widget_search_', '' );
+ eventName = eventName.replace( /_/g, '-' );
+ analytics.mc.bumpStat( 'jetpack-search-widget', eventName );
+ }
+ }
+} )( jQuery, jetpack_search_filter_admin );
diff --git a/plugins/jetpack/modules/widgets/search/js/search-widget.js b/plugins/jetpack/modules/widgets/search/js/search-widget.js
new file mode 100644
index 00000000..31e55731
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/search/js/search-widget.js
@@ -0,0 +1,19 @@
+jQuery( document ).ready( function() {
+ var filter_list = jQuery( '.jetpack-search-filters-widget__filter-list' );
+
+ filter_list.on( 'click', 'a', function() {
+ var checkbox = jQuery( this ).siblings( 'input[type="checkbox"]' );
+ checkbox.prop( 'checked', ! checkbox.prop( 'checked' ) );
+ } );
+
+ filter_list
+ .find( 'input[type="checkbox"]' )
+ .prop( 'disabled', false )
+ .css( 'cursor', 'inherit' )
+ .on( 'click', function() {
+ var anchor = jQuery( this ).siblings( 'a' );
+ if ( anchor.length ) {
+ window.location.href = anchor.prop( 'href' );
+ }
+ } );
+} );
diff --git a/plugins/jetpack/modules/widgets/simple-payments.php b/plugins/jetpack/modules/widgets/simple-payments.php
new file mode 100644
index 00000000..4eb60bdb
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/simple-payments.php
@@ -0,0 +1,544 @@
+<?php
+/**
+ * Disable direct access/execution to/of the widget code.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+if ( ! class_exists( 'Jetpack_Simple_Payments_Widget' ) ) {
+ /**
+ * Simple Payments Button
+ *
+ * Display a Simple Payments Button as a Widget.
+ */
+ class Jetpack_Simple_Payments_Widget extends WP_Widget {
+ // https://developer.paypal.com/docs/integration/direct/rest/currency-codes/
+ private static $supported_currency_list = array(
+ 'USD' => '$',
+ 'GBP' => '&#163;',
+ 'JPY' => '&#165;',
+ 'BRL' => 'R$',
+ 'EUR' => '&#8364;',
+ 'NZD' => 'NZ$',
+ 'AUD' => 'A$',
+ 'CAD' => 'C$',
+ 'INR' => '₹',
+ 'ILS' => '₪',
+ 'RUB' => '₽',
+ 'MXN' => 'MX$',
+ 'SEK' => 'Skr',
+ 'HUF' => 'Ft',
+ 'CHF' => 'CHF',
+ 'CZK' => 'Kč',
+ 'DKK' => 'Dkr',
+ 'HKD' => 'HK$',
+ 'NOK' => 'Kr',
+ 'PHP' => '₱',
+ 'PLN' => 'PLN',
+ 'SGD' => 'S$',
+ 'TWD' => 'NT$',
+ 'THB' => '฿',
+ );
+
+ /**
+ * Constructor.
+ */
+ function __construct() {
+ parent::__construct(
+ 'jetpack_simple_payments_widget',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Simple Payments', 'jetpack' ) ),
+ array(
+ 'classname' => 'jetpack-simple-payments',
+ 'description' => __( 'Add a Simple Payments Button as a Widget.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+
+ global $pagenow;
+ if ( is_customize_preview() || 'widgets.php' === $pagenow ) {
+ add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_styles' ) );
+ }
+
+ $jetpack_simple_payments = Jetpack_Simple_Payments::getInstance();
+ if ( is_customize_preview() && $jetpack_simple_payments->is_enabled_jetpack_simple_payments() ) {
+ add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
+
+ add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) );
+ add_action( 'wp_ajax_customize-jetpack-simple-payments-buttons-get', array( $this, 'ajax_get_payment_buttons' ) );
+ add_action( 'wp_ajax_customize-jetpack-simple-payments-button-save', array( $this, 'ajax_save_payment_button' ) );
+ add_action( 'wp_ajax_customize-jetpack-simple-payments-button-delete', array( $this, 'ajax_delete_payment_button' ) );
+ }
+
+ if ( is_active_widget( false, false, $this->id_base ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_style' ) );
+ }
+ }
+
+ /**
+ * Return an associative array of default values.
+ *
+ * These values are used in new widgets.
+ *
+ * @return array Default values for the widget options.
+ */
+ private function defaults() {
+ $current_user = wp_get_current_user();
+ $default_product_id = $this->get_first_product_id();
+
+ return array(
+ 'title' => '',
+ 'product_post_id' => $default_product_id,
+ 'form_action' => '',
+ 'form_product_id' => 0,
+ 'form_product_title' => '',
+ 'form_product_description' => '',
+ 'form_product_image_id' => 0,
+ 'form_product_image_src' => '',
+ 'form_product_currency' => '',
+ 'form_product_price' => '',
+ 'form_product_multiple' => '',
+ 'form_product_email' => $current_user->user_email,
+ );
+ }
+
+ /**
+ * Adds a nonce for customizing menus.
+ *
+ * @param array $nonces Array of nonces.
+ * @return array $nonces Modified array of nonces.
+ */
+ function filter_nonces( $nonces ) {
+ $nonces['customize-jetpack-simple-payments'] = wp_create_nonce( 'customize-jetpack-simple-payments' );
+ return $nonces;
+ }
+
+ function enqueue_style() {
+ wp_enqueue_style( 'jetpack-simple-payments-widget-style', plugins_url( 'simple-payments/style.css', __FILE__ ), array(), '20180518' );
+ }
+
+ function admin_enqueue_styles() {
+ wp_enqueue_style( 'jetpack-simple-payments-widget-customizer', plugins_url( 'simple-payments/customizer.css', __FILE__ ) );
+ }
+
+ function admin_enqueue_scripts() {
+ wp_enqueue_media();
+ wp_enqueue_script( 'jetpack-simple-payments-widget-customizer', plugins_url( '/simple-payments/customizer.js', __FILE__ ), array( 'jquery' ), false, true );
+ wp_localize_script(
+ 'jetpack-simple-payments-widget-customizer', 'jpSimplePaymentsStrings', array(
+ 'deleteConfirmation' => __( 'Are you sure you want to delete this item? It will be disabled and removed from all locations where it currently appears.', 'jetpack' ),
+ )
+ );
+ }
+
+ public function ajax_get_payment_buttons() {
+ if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) {
+ wp_send_json_error( 'bad_nonce', 400 );
+ }
+
+ if ( ! current_user_can( 'customize' ) ) {
+ wp_send_json_error( 'customize_not_allowed', 403 );
+ }
+
+ $post_type_object = get_post_type_object( Jetpack_Simple_Payments::$post_type_product );
+ if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) {
+ wp_send_json_error( 'insufficient_post_permissions', 403 );
+ }
+
+ $product_posts = get_posts(
+ array(
+ 'numberposts' => 100,
+ 'orderby' => 'date',
+ 'post_type' => Jetpack_Simple_Payments::$post_type_product,
+ 'post_status' => 'publish',
+ )
+ );
+
+ $formatted_products = array_map( array( $this, 'format_product_post_for_ajax_reponse' ), $product_posts );
+
+ wp_send_json_success( $formatted_products );
+ }
+
+ public function format_product_post_for_ajax_reponse( $product_post ) {
+ return array(
+ 'ID' => $product_post->ID,
+ 'post_title' => $product_post->post_title,
+ );
+ }
+
+ public function ajax_save_payment_button() {
+ if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) {
+ wp_send_json_error( 'bad_nonce', 400 );
+ }
+
+ if ( ! current_user_can( 'customize' ) ) {
+ wp_send_json_error( 'customize_not_allowed', 403 );
+ }
+
+ $post_type_object = get_post_type_object( Jetpack_Simple_Payments::$post_type_product );
+ if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) {
+ wp_send_json_error( 'insufficient_post_permissions', 403 );
+ }
+
+ if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
+ wp_send_json_error( 'missing_params', 400 );
+ }
+
+ $params = wp_unslash( $_POST['params'] );
+ $errors = $this->validate_ajax_params( $params );
+ if ( ! empty( $errors->errors ) ) {
+ wp_send_json_error( $errors );
+ }
+
+ $product_post_id = isset( $params['product_post_id'] ) ? intval( $params['product_post_id'] ) : 0;
+
+ $product_post = array(
+ 'ID' => $product_post_id,
+ 'post_type' => Jetpack_Simple_Payments::$post_type_product,
+ 'post_status' => 'publish',
+ 'post_title' => $params['post_title'],
+ 'post_content' => $params['post_content'],
+ '_thumbnail_id' => ! empty( $params['image_id'] ) ? $params['image_id'] : -1,
+ 'meta_input' => array(
+ 'spay_currency' => $params['currency'],
+ 'spay_price' => $params['price'],
+ 'spay_multiple' => isset( $params['multiple'] ) ? intval( $params['multiple'] ) : 0,
+ 'spay_email' => is_email( $params['email'] ),
+ ),
+ );
+
+ if ( empty( $product_post_id ) ) {
+ $product_post_id = wp_insert_post( $product_post );
+ } else {
+ $product_post_id = wp_update_post( $product_post );
+ }
+
+ if ( ! $product_post_id || is_wp_error( $product_post_id ) ) {
+ wp_send_json_error( $product_post_id );
+ }
+
+ $tracks_properties = array(
+ 'id' => $product_post_id,
+ 'currency' => $params['currency'],
+ 'price' => $params['price'],
+ );
+ if ( 0 === $product_post['ID'] ) {
+ $this->record_event( 'created', 'create', $tracks_properties );
+ } else {
+ $this->record_event( 'updated', 'update', $tracks_properties );
+ }
+
+ wp_send_json_success(
+ array(
+ 'product_post_id' => $product_post_id,
+ 'product_post_title' => $params['post_title'],
+ )
+ );
+ }
+
+ public function ajax_delete_payment_button() {
+ if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) {
+ wp_send_json_error( 'bad_nonce', 400 );
+ }
+
+ if ( ! current_user_can( 'customize' ) ) {
+ wp_send_json_error( 'customize_not_allowed', 403 );
+ }
+
+ if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
+ wp_send_json_error( 'missing_params', 400 );
+ }
+
+ $params = wp_unslash( $_POST['params'] );
+ $illegal_params = array_diff( array_keys( $params ), array( 'product_post_id' ) );
+ if ( ! empty( $illegal_params ) ) {
+ wp_send_json_error( 'illegal_params', 400 );
+ }
+
+ $product_id = (int) $params['product_post_id'];
+ $product_post = get_post( $product_id );
+
+ $return = array( 'status' => $product_post->post_status );
+
+ wp_delete_post( $product_id, true );
+ $status = get_post_status( $product_id );
+ if ( false === $status ) {
+ $return['status'] = 'deleted';
+ }
+
+ $this->record_event( 'deleted', 'delete', array( 'id' => $product_id ) );
+
+ wp_send_json_success( $return );
+ }
+
+ /**
+ * Returns the number of decimal places on string representing a price.
+ *
+ * @param string $number Price to check.
+ * @return number number of decimal places.
+ */
+ private function get_decimal_places( $number ) {
+ $parts = explode( '.', $number );
+ if ( count( $parts ) > 2 ) {
+ return null;
+ }
+
+ return isset( $parts[1] ) ? strlen( $parts[1] ) : 0;
+ }
+
+ public function validate_ajax_params( $params ) {
+ $errors = new WP_Error();
+
+ $illegal_params = array_diff( array_keys( $params ), array( 'product_post_id', 'post_title', 'post_content', 'image_id', 'currency', 'price', 'multiple', 'email' ) );
+ if ( ! empty( $illegal_params ) ) {
+ $errors->add( 'illegal_params', __( 'Invalid parameters.', 'jetpack' ) );
+ }
+
+ if ( empty( $params['post_title'] ) ) {
+ $errors->add( 'post_title', __( "People need to know what they're paying for! Please add a brief title.", 'jetpack' ) );
+ }
+
+ if ( empty( $params['price'] ) || ! is_numeric( $params['price'] ) || floatval( $params['price'] ) <= 0 ) {
+ $errors->add( 'price', __( 'Everything comes with a price tag these days. Please add a your product price.', 'jetpack' ) );
+ }
+
+ // Japan's Yen is the only supported currency with a zero decimal precision.
+ $precision = strtoupper( $params['currency'] ) === 'JPY' ? 0 : 2;
+ $price_decimal_places = $this->get_decimal_places( $params['price'] );
+ if ( is_null( $price_decimal_places ) || $price_decimal_places > $precision ) {
+ $errors->add( 'price', __( 'Invalid price', 'jetpack' ) );
+ }
+
+ if ( empty( $params['email'] ) || ! is_email( $params['email'] ) ) {
+ $errors->add( 'email', __( 'We want to make sure payments reach you, so please add an email address.', 'jetpack' ) );
+ }
+
+ return $errors;
+ }
+
+ function get_first_product_id() {
+ $product_posts = get_posts(
+ array(
+ 'numberposts' => 1,
+ 'orderby' => 'date',
+ 'post_type' => Jetpack_Simple_Payments::$post_type_product,
+ 'post_status' => 'publish',
+ )
+ );
+
+ return ! empty( $product_posts ) ? $product_posts[0]->ID : null;
+ }
+
+ /**
+ * Front-end display of widget.
+ *
+ * @see WP_Widget::widget()
+ *
+ * @param array $args Widget arguments.
+ * @param array $instance Saved values from database.
+ */
+ function widget( $args, $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults() );
+
+ echo $args['before_widget'];
+
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', $instance['title'] );
+ if ( ! empty( $title ) ) {
+ echo $args['before_title'] . $title . $args['after_title'];
+ }
+
+ echo '<div class="jetpack-simple-payments-content">';
+
+ if ( ! empty( $instance['form_action'] ) && in_array( $instance['form_action'], array( 'add', 'edit' ) ) && is_customize_preview() ) {
+ require( dirname( __FILE__ ) . '/simple-payments/widget.php' );
+ } else {
+ $jsp = Jetpack_Simple_Payments::getInstance();
+ $simple_payments_button = $jsp->parse_shortcode(
+ array(
+ 'id' => $instance['product_post_id'],
+ )
+ );
+
+ if ( ! is_null( $simple_payments_button ) || is_customize_preview() ) {
+ echo $simple_payments_button;
+ }
+ }
+
+ echo '</div><!--simple-payments-->';
+
+ echo $args['after_widget'];
+
+ /** This action is already documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'simple_payments' );
+ }
+
+ /**
+ * Gets the latests field value from either the old instance or the new instance.
+ *
+ * @param array $mixed Array of values for the new form instance.
+ * @param array $mixed Array of values for the old form instance.
+ * @return mixed $mixed Field value.
+ */
+ private function get_latest_field_value( $new_instance, $old_instance, $field ) {
+ return ! empty( $new_instance[ $field ] )
+ ? sanitize_text_field( $new_instance[ $field ] )
+ : $old_instance[ $field ];
+ }
+
+ /**
+ * Gets the product fields from the product post. If no post found
+ * it returns the default values.
+ *
+ * @param int Product Post ID.
+ * @return array $fields Product Fields from the Product Post.
+ */
+ private function get_product_from_post( $product_post_id ) {
+ $product_post = get_post( $product_post_id );
+ $form_product_id = $product_post_id;
+ if ( ! empty( $product_post ) ) {
+ $form_product_image_id = get_post_thumbnail_id( $product_post_id );
+
+ return array(
+ 'form_product_id' => $form_product_id,
+ 'form_product_title' => get_the_title( $product_post ),
+ 'form_product_description' => $product_post->post_content,
+ 'form_product_image_id' => $form_product_image_id,
+ 'form_product_image_src' => wp_get_attachment_image_url( $form_product_image_id, 'thumbnail' ),
+ 'form_product_currency' => get_post_meta( $product_post_id, 'spay_currency', true ),
+ 'form_product_price' => get_post_meta( $product_post_id, 'spay_price', true ),
+ 'form_product_multiple' => get_post_meta( $product_post_id, 'spay_multiple', true ) || '0',
+ 'form_product_email' => get_post_meta( $product_post_id, 'spay_email', true ),
+ );
+ }
+
+ return $this->defaults();
+ }
+
+ /**
+ * Record a Track event and bump a MC stat.
+ *
+ * @param string $stat_name
+ * @param string $event_action
+ * @param array $event_properties
+ */
+ private function record_event( $stat_name, $event_action, $event_properties = array() ) {
+ $current_user = wp_get_current_user();
+
+ // `bumps_stats_extra` only exists on .com
+ if ( function_exists( 'bump_stats_extras' ) ) {
+ require_lib( 'tracks/client' );
+ tracks_record_event( $current_user, 'simple_payments_button_' . $event_action, $event_properties );
+ /** This action is documented in modules/widgets/social-media-icons.php */
+ do_action( 'jetpack_bump_stats_extra', 'jetpack-simple_payments', $stat_name );
+ return;
+ }
+
+ jetpack_tracks_record_event( $current_user, 'jetpack_wpa_simple_payments_button_' . $event_action, $event_properties );
+ $jetpack = Jetpack::init();
+ // $jetpack->stat automatically prepends the stat group with 'jetpack-'
+ $jetpack->stat( 'simple_payments', $stat_name );
+ $jetpack->do_stats( 'server_side' );
+ }
+
+ /**
+ * Sanitize widget form values as they are saved.
+ *
+ * @see WP_Widget::update()
+ *
+ * @param array $new_instance Values just sent to be saved.
+ * @param array $old_instance Previously saved values from database.
+ *
+ * @return array Updated safe values to be saved.
+ */
+ function update( $new_instance, $old_instance ) {
+ $defaults = $this->defaults();
+ //do not overrite `product_post_id` for `$new_instance` with the defaults
+ $new_instance = wp_parse_args( $new_instance, array_diff_key( $defaults, array( 'product_post_id' => 0 ) ) );
+ $old_instance = wp_parse_args( $old_instance, $defaults );
+
+ $required_widget_props = array(
+ 'title' => $this->get_latest_field_value( $new_instance, $old_instance, 'title' ),
+ 'product_post_id' => $this->get_latest_field_value( $new_instance, $old_instance, 'product_post_id' ),
+ 'form_action' => $this->get_latest_field_value( $new_instance, $old_instance, 'form_action' ),
+ );
+
+ if ( strcmp( $new_instance['form_action'], $old_instance['form_action'] ) !== 0 ) {
+ if ( $new_instance['form_action'] == 'edit' ) {
+ return array_merge( $this->get_product_from_post( (int) $old_instance['product_post_id'] ), $required_widget_props );
+ }
+
+ if ( $new_instance['form_action'] == 'clear' ) {
+ return array_merge( $this->defaults(), $required_widget_props );
+ }
+ }
+
+ $form_product_image_id = (int) $new_instance['form_product_image_id'];
+
+ $form_product_email = ! empty( $new_instance['form_product_email'] )
+ ? sanitize_text_field( $new_instance['form_product_email'] )
+ : $defaults['form_product_email'];
+
+ return array_merge(
+ $required_widget_props, array(
+ 'form_product_id' => (int) $new_instance['form_product_id'],
+ 'form_product_title' => sanitize_text_field( $new_instance['form_product_title'] ),
+ 'form_product_description' => sanitize_text_field( $new_instance['form_product_description'] ),
+ 'form_product_image_id' => $form_product_image_id,
+ 'form_product_image_src' => wp_get_attachment_image_url( $form_product_image_id, 'thumbnail' ),
+ 'form_product_currency' => sanitize_text_field( $new_instance['form_product_currency'] ),
+ 'form_product_price' => sanitize_text_field( $new_instance['form_product_price'] ),
+ 'form_product_multiple' => sanitize_text_field( $new_instance['form_product_multiple'] ),
+ 'form_product_email' => $form_product_email,
+ )
+ );
+ }
+
+ /**
+ * Back-end widget form.
+ *
+ * @see WP_Widget::form()
+ *
+ * @param array $instance Previously saved values from database.
+ */
+ function form( $instance ) {
+ $jetpack_simple_payments = Jetpack_Simple_Payments::getInstance();
+ if ( ! method_exists( $jetpack_simple_payments, 'is_enabled_jetpack_simple_payments' ) ) {
+ return;
+ }
+ if ( ! $jetpack_simple_payments->is_enabled_jetpack_simple_payments() ) {
+ require dirname( __FILE__ ) . '/simple-payments/admin-warning.php';
+ return;
+ }
+
+ $instance = wp_parse_args( $instance, $this->defaults() );
+
+ $product_posts = get_posts(
+ array(
+ 'numberposts' => 100,
+ 'orderby' => 'date',
+ 'post_type' => Jetpack_Simple_Payments::$post_type_product,
+ 'post_status' => 'publish',
+ )
+ );
+
+ require dirname( __FILE__ ) . '/simple-payments/form.php';
+ }
+ }
+
+ // Register Jetpack_Simple_Payments_Widget widget.
+ function register_widget_jetpack_simple_payments() {
+ if ( ! class_exists( 'Jetpack_Simple_Payments' ) ) {
+ return;
+ }
+
+ $jetpack_simple_payments = Jetpack_Simple_Payments::getInstance();
+ if ( ! $jetpack_simple_payments->is_enabled_jetpack_simple_payments() ) {
+ return;
+ }
+
+ register_widget( 'Jetpack_Simple_Payments_Widget' );
+ }
+ add_action( 'widgets_init', 'register_widget_jetpack_simple_payments' );
+}
diff --git a/plugins/jetpack/modules/widgets/simple-payments/admin-warning.php b/plugins/jetpack/modules/widgets/simple-payments/admin-warning.php
new file mode 100644
index 00000000..f66e4413
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/simple-payments/admin-warning.php
@@ -0,0 +1,16 @@
+<div class='jetpack-simple-payments-disabled-error'>
+ <p>
+ <?php
+ $support_url = ( defined( 'IS_WPCOM' ) && IS_WPCOM )
+ ? 'https://support.wordpress.com/simple-payments/'
+ : 'https://jetpack.com/support/simple-payment-button/';
+ printf(
+ wp_kses(
+ __( 'Your plan doesn\'t include Simple Payments. <a href="%s" rel="noopener noreferrer" target="_blank">Learn more and upgrade</a>.', 'jetpack' ),
+ array( 'a' => array( 'href' => array(), 'rel' => array(), 'target' => array() ) )
+ ),
+ esc_url( $support_url )
+ );
+ ?>
+ </p>
+</div>
diff --git a/plugins/jetpack/modules/widgets/simple-payments/customizer.css b/plugins/jetpack/modules/widgets/simple-payments/customizer.css
new file mode 100644
index 00000000..12278a60
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/simple-payments/customizer.css
@@ -0,0 +1,80 @@
+.widget-content .jetpack-simple-payments,
+.widget-content .jetpack-simple-payments-form {
+ clear: both;
+}
+
+.widget-content .jetpack-simple-payments-disabled-error {
+ background: #fff;
+ border-left: 4px solid #dc3232;
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
+ margin: 5px 0 15px;
+ padding: 1px 12px;
+}
+
+.widget-content .jetpack-simple-payments-form .invalid {
+ border: 1px solid #dc3232;
+}
+
+.widget-content .jetpack-simple-payments-form .cost label {
+ display: block;
+}
+
+.widget-content .jetpack-simple-payments-image-fieldset {
+ position: relative;
+ width: 100%;
+}
+
+.widget-content .jetpack-simple-payments-image-fieldset .placeholder {
+ border: 1px dashed #b4b9be;
+ box-sizing: border-box;
+ cursor: pointer;
+ line-height: 20px;
+ padding: 9px 0;
+ position: relative;
+ text-align: center;
+ width: 100%;
+ margin: 4px 0 1em;
+}
+
+.widget-content .jetpack-simple-payments-image {
+ max-width: 100%;
+ margin-top: 4px;
+ position: relative;
+ text-align: center;
+}
+
+.widget-content .jetpack-simple-payments-image img {
+ max-width: 100%;
+ box-sizing: border-box;
+ border: 1px dashed #b4b9be;
+ padding: 4px;
+ height: auto;
+ cursor: pointer;
+}
+
+.widget-content .jetpack-simple-payments-image img:hover {
+ border-style: solid;
+}
+
+.widget-content .jetpack-simple-payments-form .field-currency {
+ display: inline-block;
+ vertical-align: top;
+ width: 40%;
+}
+
+.widget-content .jetpack-simple-payments-form .field-price {
+ display: inline-block;
+ line-height: 20px;
+ width: 58%;
+}
+
+.widget-content .jetpack-simple-payments-form .alignleft button,
+.widget-content .jetpack-simple-payments-form .alignright span {
+ display: inline-block;
+ margin-top: 5px;
+}
+
+.widget-content .button-link:disabled,
+.widget-content .button-link:hover[disabled] {
+ color: #a0a5aa;
+}
diff --git a/plugins/jetpack/modules/widgets/simple-payments/customizer.js b/plugins/jetpack/modules/widgets/simple-payments/customizer.js
new file mode 100644
index 00000000..c529db34
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/simple-payments/customizer.js
@@ -0,0 +1,455 @@
+/* global jQuery, jpSimplePaymentsStrings, confirm, _ */
+/* eslint no-var: 0, quote-props: 0 */
+
+( function( api, wp, $ ) {
+ var $document = $( document );
+
+ $document.ready( function() {
+ $document.on( 'widget-added', function( event, widgetContainer ) {
+ if ( widgetContainer.is( '[id*="jetpack_simple_payments_widget"]' ) ) {
+ initWidget( widgetContainer );
+ }
+ } );
+
+ $document.on( 'widget-synced widget-updated', function( event, widgetContainer ) {
+ //this fires for all widgets, this prevent errors for non SP widgets
+ if ( ! widgetContainer.is( '[id*="jetpack_simple_payments_widget"]' ) ) {
+ return;
+ }
+
+ event.preventDefault();
+
+ syncProductLists();
+
+ var widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
+
+ enableFormActions( widgetForm );
+
+ updateProductImage( widgetForm );
+ } );
+ } );
+
+ function initWidget( widgetContainer ) {
+ var widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
+
+ //Add New Button
+ widgetForm
+ .find( '.jetpack-simple-payments-add-product' )
+ .on( 'click', showAddNewForm( widgetForm ) );
+ //Edit Button
+ widgetForm
+ .find( '.jetpack-simple-payments-edit-product' )
+ .on( 'click', showEditForm( widgetForm ) );
+ //Select an Image
+ widgetForm
+ .find(
+ '.jetpack-simple-payments-image-fieldset .placeholder, .jetpack-simple-payments-image > img'
+ )
+ .on( 'click', selectImage( widgetForm ) );
+ //Remove Image Button
+ widgetForm
+ .find( '.jetpack-simple-payments-remove-image' )
+ .on( 'click', removeImage( widgetForm ) );
+ //Save Product button
+ widgetForm
+ .find( '.jetpack-simple-payments-save-product' )
+ .on( 'click', saveChanges( widgetForm ) );
+ //Cancel Button
+ widgetForm
+ .find( '.jetpack-simple-payments-cancel-form' )
+ .on( 'click', clearForm( widgetForm ) );
+ //Delete Selected Product
+ widgetForm
+ .find( '.jetpack-simple-payments-delete-product' )
+ .on( 'click', deleteProduct( widgetForm ) );
+ //Input, Select and Checkbox change
+ widgetForm.find( 'select, input, textarea, checkbox' ).on(
+ 'change input propertychange',
+ _.debounce( function() {
+ disableFormActions( widgetForm );
+ }, 250 )
+ );
+ }
+
+ function syncProductLists() {
+ var request = wp.ajax.post( 'customize-jetpack-simple-payments-buttons-get', {
+ 'customize-jetpack-simple-payments-nonce':
+ api.settings.nonce[ 'customize-jetpack-simple-payments' ],
+ customize_changeset_uuid: api.settings.changeset.uuid,
+ } );
+
+ request.done( function( data ) {
+ var selectedProduct = 0;
+
+ $( document )
+ .find( 'select.jetpack-simple-payments-products' )
+ .each( function( index, select ) {
+ var $select = $( select );
+ selectedProduct = $select.val();
+
+ $select.find( 'option' ).remove();
+ $select.append(
+ $.map( data, function( product ) {
+ return $( '<option>', { value: product.ID, text: product.post_title } );
+ } )
+ );
+ $select.val( selectedProduct );
+ } );
+ } );
+ }
+
+ function showForm( widgetForm ) {
+ //reset validations
+ widgetForm.find( '.invalid' ).removeClass( 'invalid' );
+ //disable widget title and product selector
+ widgetForm
+ .find( '.jetpack-simple-payments-widget-title' )
+ .add( '.jetpack-simple-payments-products' )
+ //disable add and edit buttons
+ .add( '.jetpack-simple-payments-add-product' )
+ .add( '.jetpack-simple-payments-edit-product' )
+ //disable save, delete and cancel until the widget update event is fired
+ .add( '.jetpack-simple-payments-save-product' )
+ .add( '.jetpack-simple-payments-cancel-form' )
+ .add( '.jetpack-simple-payments-delete-product' )
+ .attr( 'disabled', 'disabled' );
+ //show form
+ widgetForm.find( '.jetpack-simple-payments-form' ).show();
+ }
+
+ function hideForm( widgetForm ) {
+ //enable widget title and product selector
+ widgetForm
+ .find( '.jetpack-simple-payments-widget-title' )
+ .add( '.jetpack-simple-payments-products' )
+ .removeAttr( 'disabled' );
+ //hide the form
+ widgetForm.find( '.jetpack-simple-payments-form' ).hide();
+ }
+
+ function changeFormAction( widgetForm, action ) {
+ widgetForm
+ .find( '.jetpack-simple-payments-form-action' )
+ .val( action )
+ .change();
+ }
+
+ function showAddNewForm( widgetForm ) {
+ return function( event ) {
+ event.preventDefault();
+
+ showForm( widgetForm );
+ changeFormAction( widgetForm, 'add' );
+ };
+ }
+
+ function showEditForm( widgetForm ) {
+ return function( event ) {
+ event.preventDefault();
+
+ showForm( widgetForm );
+ changeFormAction( widgetForm, 'edit' );
+ };
+ }
+
+ function clearForm( widgetForm ) {
+ return function( event ) {
+ event.preventDefault();
+
+ hideForm( widgetForm );
+ widgetForm
+ .find( '.jetpack-simple-payments-add-product, .jetpack-simple-payments-edit-product' )
+ .attr( 'disabled', 'disabled' );
+ changeFormAction( widgetForm, 'clear' );
+ };
+ }
+
+ function enableFormActions( widgetForm ) {
+ var isFormVisible = widgetForm.find( '.jetpack-simple-payments-form' ).is( ':visible' );
+ var isProductSelectVisible = widgetForm
+ .find( '.jetpack-simple-payments-products' )
+ .is( ':visible' ); //areProductsVisible ?
+ var isEdit = widgetForm.find( '.jetpack-simple-payments-form-action' ).val() === 'edit';
+
+ if ( isFormVisible ) {
+ widgetForm
+ .find( '.jetpack-simple-payments-save-product' )
+ .add( '.jetpack-simple-payments-cancel-form' )
+ .removeAttr( 'disabled' );
+ } else {
+ widgetForm.find( '.jetpack-simple-payments-add-product' ).removeAttr( 'disabled' );
+ }
+
+ if ( isFormVisible && isEdit ) {
+ widgetForm.find( '.jetpack-simple-payments-delete-product' ).removeAttr( 'disabled' );
+ }
+
+ if ( isProductSelectVisible && ! isFormVisible ) {
+ widgetForm.find( '.jetpack-simple-payments-edit-product' ).removeAttr( 'disabled' );
+ }
+ }
+
+ function disableFormActions( widgetForm ) {
+ widgetForm
+ .find( '.jetpack-simple-payments-add-product' )
+ .add( '.jetpack-simple-payments-edit-product' )
+ .add( '.jetpack-simple-payments-save-product' )
+ .add( '.jetpack-simple-payments-cancel-form' )
+ .add( '.jetpack-simple-payments-delete-product' )
+ .attr( 'disabled', 'disabled' );
+ }
+
+ function selectImage( widgetForm ) {
+ return function( event ) {
+ event.preventDefault();
+
+ var imageContainer = widgetForm.find( '.jetpack-simple-payments-image' );
+
+ var mediaFrame = new wp.media.view.MediaFrame.Select( {
+ title: 'Choose Product Image',
+ multiple: false,
+ library: { type: 'image' },
+ button: { text: 'Choose Image' },
+ } );
+
+ mediaFrame.on( 'select', function() {
+ var selection = mediaFrame
+ .state()
+ .get( 'selection' )
+ .first()
+ .toJSON();
+ //hide placeholder
+ widgetForm.find( '.jetpack-simple-payments-image-fieldset .placeholder' ).hide();
+
+ //load image from media library
+ imageContainer
+ .find( 'img' )
+ .attr( 'src', selection.url )
+ .show();
+
+ //show image and remove button
+ widgetForm.find( '.jetpack-simple-payments-image' ).show();
+
+ //set hidden field for the selective refresh
+ widgetForm
+ .find( '.jetpack-simple-payments-form-image-id' )
+ .val( selection.id )
+ .change();
+ } );
+
+ mediaFrame.open();
+ };
+ }
+
+ function removeImage( widgetForm ) {
+ return function( event ) {
+ event.preventDefault();
+
+ //show placeholder
+ widgetForm.find( '.jetpack-simple-payments-image-fieldset .placeholder' ).show();
+
+ //hide image and remove button
+ widgetForm.find( '.jetpack-simple-payments-image' ).hide();
+
+ //set hidden field for the selective refresh
+ widgetForm
+ .find( '.jetpack-simple-payments-form-image-id' )
+ .val( '' )
+ .change();
+ };
+ }
+
+ function updateProductImage( widgetForm ) {
+ var newImageId = parseInt(
+ widgetForm.find( '.jetpack-simple-payments-form-image-id' ).val(),
+ 10
+ );
+ var newImageSrc = widgetForm.find( '.jetpack-simple-payments-form-image-src' ).val();
+
+ var placeholder = widgetForm.find( '.jetpack-simple-payments-image-fieldset .placeholder' );
+ var image = widgetForm.find( '.jetpack-simple-payments-image > img' );
+ var imageControls = widgetForm.find( '.jetpack-simple-payments-image' );
+
+ if ( newImageId && newImageSrc ) {
+ image.attr( 'src', newImageSrc );
+ placeholder.hide();
+ imageControls.show();
+ } else {
+ placeholder.show();
+ image.removeAttr( 'src' );
+ imageControls.hide();
+ }
+ }
+
+ function decimalPlaces( number ) {
+ var parts = number.split( '.' );
+ if ( parts.length > 2 ) {
+ return null;
+ }
+
+ return parts[ 1 ] ? parts[ 1 ].length : 0;
+ }
+
+ function isFormValid( widgetForm ) {
+ widgetForm.find( '.invalid' ).removeClass( 'invalid' );
+
+ var errors = false;
+
+ var postTitle = widgetForm.find( '.jetpack-simple-payments-form-product-title' ).val();
+ if ( ! postTitle ) {
+ widgetForm.find( '.jetpack-simple-payments-form-product-title' ).addClass( 'invalid' );
+ errors = true;
+ }
+
+ var productPrice = widgetForm.find( '.jetpack-simple-payments-form-product-price' ).val();
+ if ( ! productPrice || isNaN( productPrice ) || parseFloat( productPrice ) <= 0 ) {
+ widgetForm.find( '.jetpack-simple-payments-form-product-price' ).addClass( 'invalid' );
+ errors = true;
+ }
+
+ // Japan's Yen is the only supported currency with a zero decimal precision.
+ var precision =
+ widgetForm.find( '.jetpack-simple-payments-form-product-currency' ).val() === 'JPY' ? 0 : 2;
+ var priceDecimalPlaces = decimalPlaces( productPrice );
+ if ( priceDecimalPlaces === null || priceDecimalPlaces > precision ) {
+ widgetForm.find( '.jetpack-simple-payments-form-product-price' ).addClass( 'invalid' );
+ errors = true;
+ }
+
+ var productEmail = widgetForm.find( '.jetpack-simple-payments-form-product-email' ).val();
+ var isProductEmailValid = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(
+ productEmail
+ );
+ if ( ! productEmail || ! isProductEmailValid ) {
+ widgetForm.find( '.jetpack-simple-payments-form-product-email' ).addClass( 'invalid' );
+ errors = true;
+ }
+
+ return ! errors;
+ }
+
+ function saveChanges( widgetForm ) {
+ return function( event ) {
+ event.preventDefault();
+ var productPostId = widgetForm.find( '.jetpack-simple-payments-form-product-id' ).val();
+
+ if ( ! isFormValid( widgetForm ) ) {
+ return;
+ }
+
+ disableFormActions( widgetForm );
+
+ widgetForm.find( '.spinner' ).show();
+
+ var request = wp.ajax.post( 'customize-jetpack-simple-payments-button-save', {
+ 'customize-jetpack-simple-payments-nonce':
+ api.settings.nonce[ 'customize-jetpack-simple-payments' ],
+ customize_changeset_uuid: api.settings.changeset.uuid,
+ params: {
+ product_post_id: productPostId,
+ post_title: widgetForm.find( '.jetpack-simple-payments-form-product-title' ).val(),
+ post_content: widgetForm
+ .find( '.jetpack-simple-payments-form-product-description' )
+ .val(),
+ image_id: widgetForm.find( '.jetpack-simple-payments-form-image-id' ).val(),
+ currency: widgetForm.find( '.jetpack-simple-payments-form-product-currency' ).val(),
+ price: widgetForm.find( '.jetpack-simple-payments-form-product-price' ).val(),
+ multiple: widgetForm
+ .find( '.jetpack-simple-payments-form-product-multiple' )
+ .is( ':checked' )
+ ? 1
+ : 0,
+ email: widgetForm.find( '.jetpack-simple-payments-form-product-email' ).val(),
+ },
+ } );
+
+ request.done( function( data ) {
+ var select = widgetForm.find( 'select.jetpack-simple-payments-products' );
+ var productOption = select.find( 'option[value="' + productPostId + '"]' );
+
+ if ( productOption.length > 0 ) {
+ productOption.text( data.product_post_title );
+ } else {
+ select.append(
+ $( '<option>', {
+ value: data.product_post_id,
+ text: data.product_post_title,
+ } )
+ );
+ select.val( data.product_post_id ).change();
+ }
+
+ widgetForm.find( '.jetpack-simple-payments-products-fieldset' ).show();
+ widgetForm.find( '.jetpack-simple-payments-products-warning' ).hide();
+
+ changeFormAction( widgetForm, 'clear' );
+ hideForm( widgetForm );
+ } );
+
+ request.fail( function( data ) {
+ var validCodes = {
+ post_title: 'product-title',
+ price: 'product-price',
+ email: 'product-email',
+ };
+
+ data.forEach( function( item ) {
+ if ( validCodes.hasOwnProperty( item.code ) ) {
+ widgetForm
+ .find( '.jetpack-simple-payments-form-' + validCodes[ item.code ] )
+ .addClass( 'invalid' );
+ }
+ } );
+
+ enableFormActions( widgetForm );
+ } );
+ };
+ }
+
+ function deleteProduct( widgetForm ) {
+ return function( event ) {
+ event.preventDefault();
+
+ if ( ! confirm( jpSimplePaymentsStrings.deleteConfirmation ) ) {
+ return;
+ }
+
+ var formProductId = parseInt(
+ widgetForm.find( '.jetpack-simple-payments-form-product-id' ).val(),
+ 10
+ );
+ if ( ! formProductId ) {
+ return;
+ }
+
+ disableFormActions( widgetForm );
+
+ widgetForm.find( '.spinner' ).show();
+
+ var request = wp.ajax.post( 'customize-jetpack-simple-payments-button-delete', {
+ 'customize-jetpack-simple-payments-nonce':
+ api.settings.nonce[ 'customize-jetpack-simple-payments' ],
+ customize_changeset_uuid: api.settings.changeset.uuid,
+ params: {
+ product_post_id: formProductId,
+ },
+ } );
+
+ request.done( function() {
+ var productList = widgetForm.find( 'select.jetpack-simple-payments-products' )[ 0 ];
+ productList.remove( productList.selectedIndex );
+ productList.dispatchEvent( new Event( 'change' ) );
+
+ if ( $( productList ).has( 'option' ).length === 0 ) {
+ //hide products select and label
+ widgetForm.find( '.jetpack-simple-payments-products-fieldset' ).hide();
+ //show empty products list warning
+ widgetForm.find( '.jetpack-simple-payments-products-warning' ).show();
+ }
+
+ changeFormAction( widgetForm, 'clear' );
+ hideForm( widgetForm );
+ } );
+ };
+ }
+} )( wp.customize, wp, jQuery );
diff --git a/plugins/jetpack/modules/widgets/simple-payments/form.php b/plugins/jetpack/modules/widgets/simple-payments/form.php
new file mode 100644
index 00000000..7732be5d
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/simple-payments/form.php
@@ -0,0 +1,205 @@
+<?php
+/**
+ * Display the Simple Payments Form.
+ *
+ * @package Jetpack
+ */
+
+?>
+<p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
+ <?php esc_html_e( 'Widget Title', 'jetpack' ); ?>
+ </label>
+ <input
+ type="text"
+ class="widefat jetpack-simple-payments-widget-title"
+ id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
+ value="<?php echo esc_attr( $instance['title'] ); ?>" />
+</p>
+<p class="jetpack-simple-payments-products-fieldset" <?php if ( empty( $product_posts ) ) { echo 'style="display:none;"'; } ?>>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'product_post_id' ) ); ?>">
+ <?php esc_html_e( 'Select a Simple Payments Button:', 'jetpack' ); ?>
+ </label>
+ <select
+ class="widefat jetpack-simple-payments-products"
+ id="<?php echo esc_attr( $this->get_field_id( 'product_post_id' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'product_post_id' ) ); ?>">
+ <?php foreach ( $product_posts as $product_post ) { ?>
+ <option value="<?php echo esc_attr( $product_post->ID ); ?>" <?php selected( (int) $instance['product_post_id'], $product_post->ID ); ?>>
+ <?php echo esc_attr( get_the_title( $product_post ) ); ?>
+ </option>
+ <?php } ?>
+ </select>
+</p>
+<?php if ( is_customize_preview() ) { ?>
+<p class="jetpack-simple-payments-products-warning" <?php if ( ! empty( $product_posts ) ) { echo 'style="display:none;"'; } ?>>
+ <?php esc_html_e( "Looks like you don't have any products. You can create one using the Add New button below.", 'jetpack' ); ?>
+</p>
+<p>
+ <div class="alignleft">
+ <button class="button jetpack-simple-payments-edit-product" <?php disabled( empty( $product_posts ), true ); ?>>
+ <?php esc_html_e( 'Edit Selected', 'jetpack' ); ?>
+ </button>
+ </div>
+ <div class="alignright">
+ <button class="button jetpack-simple-payments-add-product"><?php esc_html_e( 'Add New', 'jetpack' ); ?></button>
+ </div>
+ <br class="clear">
+</p>
+<hr />
+<div class="jetpack-simple-payments-form" style="display: none;">
+ <input
+ type="hidden"
+ id="<?php echo esc_attr( $this->get_field_id( 'form_action' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'form_action' ) ); ?>"
+ value="<?php echo esc_attr( $instance['form_action'] ); ?>"
+ class="jetpack-simple-payments-form-action" />
+ <input
+ type="hidden"
+ id="<?php echo esc_attr( $this->get_field_id( 'form_product_id' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'form_product_id' ) ); ?>"
+ value="<?php echo esc_attr( $instance['form_product_id'] ); ?>"
+ class="jetpack-simple-payments-form-product-id" />
+ <input
+ type="hidden"
+ id="<?php echo esc_attr( $this->get_field_id( 'form_product_image_id' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'form_product_image_id' ) ); ?>"
+ value="<?php echo esc_attr( $instance['form_product_image_id'] ); ?>"
+ class="jetpack-simple-payments-form-image-id" />
+ <input
+ type="hidden"
+ id="<?php echo esc_attr( $this->get_field_id( 'form_product_image_src' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'form_product_image_src' ) ); ?>"
+ value="<?php echo esc_attr( $instance['form_product_image_src'] ); ?>"
+ class="jetpack-simple-payments-form-image-src" />
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'form_product_title' ) ); ?>">
+ <?php esc_html_e( 'What is this payment for?', 'jetpack' ); ?>
+ </label>
+ <input
+ type="text"
+ class="widefat field-title jetpack-simple-payments-form-product-title"
+ id="<?php echo esc_attr( $this->get_field_id( 'form_product_title' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'form_product_title' ) ); ?>"
+ value="<?php echo esc_attr( $instance['form_product_title'] ); ?>" />
+ <br />
+ <small>
+ <?php esc_html_e( 'For example: event tickets, charitable donations, training courses, coaching fees, etc.', 'jetpack' ); ?>
+ </small>
+ </p>
+ <div class="jetpack-simple-payments-image-fieldset">
+ <label><?php esc_html_e( 'Product image', 'jetpack' ); ?></label>
+ <div class="placeholder" <?php if ( ! empty( $instance['form_product_image_id'] ) ) echo 'style="display:none;"'; ?>>
+ <?php esc_html_e( 'Select an image', 'jetpack' ); ?>
+ </div>
+ <div class="jetpack-simple-payments-image" <?php if ( empty( $instance['form_product_image_id'] ) ) echo 'style="display:none;"'; ?>>
+ <img src="<?php echo esc_url( $instance['form_product_image_src'] ); ?>" />
+ <button class="button jetpack-simple-payments-remove-image"><?php esc_html_e( 'Remove image', 'jetpack' ); ?></button>
+ </div>
+ </div>
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'form_product_description' ) ); ?>">
+ <?php esc_html_e( 'Description', 'jetpack' ); ?>
+ </label>
+ <textarea
+ class="field-description widefat jetpack-simple-payments-form-product-description"
+ rows=5
+ id="<?php echo esc_attr( $this->get_field_id( 'form_product_description' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'form_product_description' ) ); ?>"><?php echo esc_textarea( $instance['form_product_description'] ); ?></textarea>
+ </p>
+ <p class="cost">
+ <label for="<?php echo esc_attr( $this->get_field_id( 'form_product_price' ) ); ?>">
+ <?php esc_html_e( 'Price', 'jetpack' ); ?>
+ </label>
+ <select
+ class="field-currency widefat jetpack-simple-payments-form-product-currency"
+ id="<?php echo esc_attr( $this->get_field_id( 'form_product_currency' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'form_product_currency' ) ); ?>">
+ <?php foreach ( Jetpack_Simple_Payments_Widget::$supported_currency_list as $code => $currency ) { ?>
+ <option value="<?php echo esc_attr( $code ); ?>"<?php selected( $instance['form_product_currency'], $code ); ?>>
+ <?php echo esc_html( "$code $currency" ); ?>
+ </option>
+ <?php } ?>
+ </select>
+ <input
+ type="text"
+ class="field-price widefat jetpack-simple-payments-form-product-price"
+ id="<?php echo esc_attr( $this->get_field_id( 'form_product_price' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'form_product_price' ) ); ?>"
+ value="<?php echo esc_attr( $instance['form_product_price'] ); ?>"
+ placeholder="1.00" />
+ </p>
+ <p>
+ <input
+ class="field-multiple jetpack-simple-payments-form-product-multiple"
+ id="<?php echo esc_attr( $this->get_field_id( 'form_product_multiple' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'form_product_multiple' ) ); ?>"
+ type="checkbox"
+ value="1"
+ <?php checked( $instance['form_product_multiple'], '1' ); ?> />
+ <label for="<?php echo esc_attr( $this->get_field_id( 'form_product_multiple' ) ); ?>">
+ <?php esc_html_e( 'Allow people to buy more than one item at a time.', 'jetpack' ); ?>
+ </label>
+ </p>
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'form_product_email' ) ); ?>">
+ <?php esc_html_e( 'Email', 'jetpack' ); ?>
+ </label>
+ <input
+ class="field-email widefat jetpack-simple-payments-form-product-email"
+ id="<?php echo esc_attr( $this->get_field_id( 'form_product_email' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'form_product_email' ) ); ?>"
+ type="email"
+ value="<?php echo esc_attr( $instance['form_product_email'] ); ?>" />
+ <small>
+ <?php
+ printf(
+ wp_kses(
+ /* Translators: placeholders are a link to Paypal website and a target attribute. */
+ __( 'This is where PayPal will send your money. To claim a payment, you\'ll need a <a href="%1$s" %2$s>PayPal account</a> connected to a bank account.', 'jetpack' ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'target' => array(),
+ ),
+ )
+ ),
+ 'https://paypal.com',
+ 'target="_blank"'
+ );
+ ?>
+ </small>
+ </p>
+ <p>
+ <div class="alignleft">
+ <button type="button" class="button-link button-link-delete jetpack-simple-payments-delete-product">
+ <?php esc_html_e( 'Delete Product', 'jetpack' ); ?>
+ </button>
+ </div>
+ <div class="alignright">
+ <button name="<?php echo esc_attr( $this->get_field_name( 'save' ) ); ?>" class="button jetpack-simple-payments-save-product"><?php esc_html_e( 'Save', 'jetpack' ); ?></button>
+ <span> | <button type="button" class="button-link jetpack-simple-payments-cancel-form"><?php esc_html_e( 'Cancel', 'jetpack' ); ?></button></span>
+ </div>
+ <br class="clear">
+ </p>
+ <hr />
+</div>
+<?php } else { ?>
+<p class="jetpack-simple-payments-products-warning">
+ <?php
+ printf(
+ wp_kses(
+ /* Translators: placeholder is a link to the customizer. */
+ __( 'This widget adds a payment button of your choice to your sidebar. To create or edit the payment buttons themselves, <a href="%s">use the Customizer</a>.', 'jetpack' ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ ),
+ )
+ ),
+ esc_url( add_query_arg( array( 'autofocus[panel]' => 'widgets' ), admin_url( 'customize.php' ) ) )
+ );
+ ?>
+</p>
+<?php } ?>
diff --git a/plugins/jetpack/modules/widgets/simple-payments/style.css b/plugins/jetpack/modules/widgets/simple-payments/style.css
new file mode 100644
index 00000000..3a701e01
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/simple-payments/style.css
@@ -0,0 +1,8 @@
+@media screen and (min-width: 400px) {
+ .widget.jetpack-simple-payments .jetpack-simple-payments-product {
+ flex-direction: column;
+ }
+ .widget.jetpack-simple-payments .jetpack-simple-payments-details {
+ padding-left: 0;
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/simple-payments/widget.php b/plugins/jetpack/modules/widgets/simple-payments/widget.php
new file mode 100644
index 00000000..001635ba
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/simple-payments/widget.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Display the Simple Payments Widget.
+ *
+ * @package Jetpack
+ */
+
+?>
+<div class='jetpack-simple-payments-wrapper'>
+ <div class='jetpack-simple-payments-product'>
+ <div class='jetpack-simple-payments-product-image' <?php if ( empty( $instance['form_product_image_id'] ) ) echo 'style="display:none;"'; ?>>
+ <div class='jetpack-simple-payments-image'>
+ <?php echo wp_get_attachment_image( $instance['form_product_image_id'], 'full' ); ?>
+ </div>
+ </div>
+ <div class='jetpack-simple-payments-details'>
+ <div class='jetpack-simple-payments-title'><p><?php echo esc_html( $instance['form_product_title'] ); ?></p></div>
+ <div class='jetpack-simple-payments-description'><p><?php echo esc_html( $instance['form_product_description'] ); ?></p></div>
+ <div class='jetpack-simple-payments-price'><p><?php echo esc_html( $instance['form_product_price'] ); ?> <?php echo esc_html( $instance['form_product_currency'] ); ?></p></div>
+ <div class='jetpack-simple-payments-purchase-box'>
+ <?php if ( $instance['form_product_multiple'] ) { ?>
+ <div class='jetpack-simple-payments-items'>
+ <input
+ type='number'
+ class='jetpack-simple-payments-items-number'
+ value='1'
+ min='1' />
+ </div>
+ <?php } ?>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/plugins/jetpack/modules/widgets/social-icons.php b/plugins/jetpack/modules/widgets/social-icons.php
new file mode 100644
index 00000000..7afbd27d
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/social-icons.php
@@ -0,0 +1,680 @@
+<?php
+class Jetpack_Widget_Social_Icons extends WP_Widget {
+ /**
+ * @var array Default widget options.
+ */
+ protected $defaults;
+
+ /**
+ * Widget constructor.
+ */
+ public function __construct() {
+ global $pagenow;
+
+ $widget_ops = array(
+ 'classname' => 'jetpack_widget_social_icons',
+ 'description' => __( 'Add social-media icons to your site.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ );
+
+ parent::__construct(
+ 'jetpack_widget_social_icons',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Social Icons', 'jetpack' ) ),
+ $widget_ops
+ );
+
+ $this->defaults = array(
+ 'title' => __( 'Follow Us', 'jetpack' ),
+ 'icon-size' => 'medium',
+ 'new-tab' => false,
+ 'icons' => array(
+ array(
+ 'url' => '',
+ ),
+ ),
+ );
+
+ // Enqueue admin scrips and styles, only in the customizer or the old widgets page.
+ if ( is_customize_preview() || 'widgets.php' === $pagenow ) {
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
+ add_action( 'admin_print_footer_scripts', array( $this, 'render_admin_js' ) );
+ }
+
+ // Enqueue scripts and styles for the display of the widget, on the frontend or in the customizer.
+ if ( is_active_widget( false, $this->id, $this->id_base, true ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_icon_scripts' ) );
+ add_action( 'wp_footer', array( $this, 'include_svg_icons' ), 9999 );
+ }
+ }
+
+ /**
+ * Script & styles for admin widget form.
+ */
+ public function enqueue_admin_scripts() {
+ wp_enqueue_script( 'jetpack-widget-social-icons-script', plugins_url( 'social-icons/social-icons-admin.js', __FILE__ ), array( 'jquery-ui-sortable' ), '20170506' );
+ wp_enqueue_style( 'jetpack-widget-social-icons-admin', plugins_url( 'social-icons/social-icons-admin.css', __FILE__ ), array(), '20170506' );
+ }
+
+ /**
+ * Styles for front-end widget.
+ */
+ public function enqueue_icon_scripts() {
+ wp_enqueue_style( 'jetpack-widget-social-icons-styles', plugins_url( 'social-icons/social-icons.css', __FILE__ ), array(), '20170506' );
+ }
+
+ /**
+ * JavaScript for admin widget form.
+ */
+ public function render_admin_js() {
+ ?>
+ <script type="text/html" id="tmpl-jetpack-widget-social-icons-template">
+ <?php self::render_icons_template(); ?>
+ </script>
+ <?php
+ }
+
+ /**
+ * Add SVG definitions to the footer.
+ */
+ public function include_svg_icons() {
+ // Define SVG sprite file in Jetpack
+ $svg_icons = dirname( dirname( __FILE__ ) ) . '/theme-tools/social-menu/social-menu.svg';
+
+ // Define SVG sprite file in WPCOM
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $svg_icons = dirname( dirname( __FILE__ ) ) . '/social-menu/social-menu.svg';
+ }
+
+ // If it exists, include it.
+ if ( is_file( $svg_icons ) ) {
+ require_once( $svg_icons );
+ }
+ }
+
+ /**
+ * Front-end display of widget.
+ *
+ * @see WP_Widget::widget()
+ *
+ * @param array $args Widget arguments.
+ * @param array $instance Saved values from database.
+ */
+ public function widget( $args, $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults );
+
+ /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
+ $title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
+
+ echo $args['before_widget'];
+
+ if ( ! empty( $title ) ) {
+ echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
+ }
+
+ if ( ! empty( $instance['icons'] ) ) :
+
+ // Get supported social icons.
+ $social_icons = $this->get_supported_icons();
+ $default_icon = $this->get_svg_icon( array( 'icon' => 'chain' ) );
+
+ // Set target attribute for the link
+ if ( true === $instance['new-tab'] ) {
+ $target = '_blank';
+ } else {
+ $target = '_self';
+ }
+ ?>
+
+ <ul class="jetpack-social-widget-list size-<?php echo esc_attr( $instance['icon-size'] ); ?>">
+
+ <?php foreach ( $instance['icons'] as $icon ) : ?>
+
+ <?php if ( ! empty( $icon['url'] ) ) : ?>
+ <li class="jetpack-social-widget-item">
+ <a href="<?php echo esc_url( $icon['url'], array( 'http', 'https', 'mailto', 'skype' ) ); ?>" target="<?php echo $target; ?>">
+ <?php
+ $found_icon = false;
+
+ foreach ( $social_icons as $social_icon ) {
+ if ( false !== stripos( $icon['url'], $social_icon['url'] ) ) {
+ echo '<span class="screen-reader-text">' . esc_attr( $social_icon['label'] ) . '</span>';
+ echo $this->get_svg_icon( array( 'icon' => esc_attr( $social_icon['icon'] ) ) );
+ $found_icon = true;
+ break;
+ }
+ }
+
+ if ( ! $found_icon ) {
+ echo $default_icon;
+ }
+ ?>
+ </a>
+ </li>
+ <?php endif; ?>
+
+ <?php endforeach; ?>
+
+ </ul>
+
+ <?php
+ endif;
+
+ echo $args['after_widget'];
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'social_icons' );
+ }
+
+ /**
+ * Sanitize widget form values as they are saved.
+ *
+ * @see WP_Widget::update()
+ *
+ * @param array $new_instance Values just sent to be saved.
+ * @param array $old_instance Previously saved values from database.
+ *
+ * @return array Updated safe values to be saved.
+ */
+ public function update( $new_instance, $old_instance ) {
+ $instance['title'] = sanitize_text_field( $new_instance['title'] );
+ $instance['icon-size'] = $this->defaults['icon-size'];
+
+ if ( in_array( $new_instance['icon-size'], array( 'small', 'medium', 'large' ) ) ) {
+ $instance['icon-size'] = $new_instance['icon-size'];
+ }
+
+ $instance['new-tab'] = isset( $new_instance['new-tab'] ) ? (bool) $new_instance['new-tab'] : false;
+ $icon_count = count( $new_instance['url-icons'] );
+ $instance['icons'] = array();
+
+ foreach ( $new_instance['url-icons'] as $url ) {
+ $url = filter_var( $url, FILTER_SANITIZE_URL );
+
+ if ( ! empty( $url ) ) {
+ $instance['icons'][] = array(
+ 'url' => $url,
+ );
+ }
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Back-end widget form.
+ *
+ * @see WP_Widget::form()
+ *
+ * @param array $instance Previously saved values from database.
+ *
+ * @return string|void
+ */
+ public function form( $instance ) {
+ $instance = wp_parse_args( $instance, $this->defaults );
+ $title = sanitize_text_field( $instance['title'] );
+ $sizes = array(
+ 'small' => __( 'Small', 'jetpack' ),
+ 'medium' => __( 'Medium', 'jetpack' ),
+ 'large' => __( 'Large', 'jetpack' ),
+ );
+ $new_tab = isset( $instance['new-tab'] ) ? (bool) $instance['new-tab'] : false;
+ ?>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php esc_html_e( 'Title:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'icon-size' ); ?>"><?php esc_html_e( 'Size:', 'jetpack' ); ?></label>
+ <select class="widefat" name="<?php echo $this->get_field_name( 'icon-size' ); ?>">
+ <?php foreach ( $sizes as $value => $label ) : ?>
+ <option value="<?php echo esc_attr( $value ); ?>" <?php selected( $value, $instance['icon-size'] ); ?>><?php echo esc_attr( $label ); ?></option>
+ <?php endforeach; ?>
+ </select>
+ </p>
+
+ <div class="jetpack-social-icons-widget-list"
+ data-url-icon-id="<?php echo $this->get_field_id( 'url-icons' ); ?>"
+ data-url-icon-name="<?php echo $this->get_field_name( 'url-icons' ); ?>"
+ >
+
+ <?php
+ foreach ( $instance['icons'] as $icon ) {
+ self::render_icons_template(
+ array(
+ 'url-icon-id' => $this->get_field_id( 'url-icons' ),
+ 'url-icon-name' => $this->get_field_name( 'url-icons' ),
+ 'url-value' => $icon['url'],
+ )
+ );
+ }
+ ?>
+
+ </div>
+
+ <p class="jetpack-social-icons-widget add-button">
+ <button type="button" class="button jetpack-social-icons-add-button">
+ <?php esc_html_e( 'Add an icon', 'jetpack' ); ?>
+ </button>
+ </p>
+
+ <?php
+ switch ( get_locale() ) {
+ case 'es':
+ $support = 'https://es.support.wordpress.com/social-media-icons-widget/#iconos-disponibles';
+ break;
+
+ case 'pt-br':
+ $support = 'https://br.support.wordpress.com/widgets/widget-de-icones-sociais/#ícones-disponíveis';
+ break;
+
+ default:
+ $support = 'https://en.support.wordpress.com/widgets/social-media-icons-widget/#available-icons';
+ }
+ ?>
+
+ <p>
+ <em><a href="<?php echo esc_url( $support ); ?>" target="_blank">
+ <?php esc_html_e( 'View available icons', 'jetpack' ); ?>
+ </a></em>
+ </p>
+
+ <p>
+ <input type="checkbox" class="checkbox" id="<?php echo $this->get_field_id( 'new-tab' ); ?>" name="<?php echo $this->get_field_name( 'new-tab' ); ?>" <?php checked( $new_tab ); ?> />
+ <label for="<?php echo $this->get_field_id( 'new-tab' ); ?>"><?php esc_html_e( 'Open link in a new tab', 'jetpack' ); ?></label>
+ </p>
+
+ <?php
+ }
+
+ /**
+ * Generates template to add icons.
+ *
+ * @param array $args Template arguments
+ */
+ static function render_icons_template( $args = array() ) {
+ $defaults = array(
+ 'url-icon-id' => '',
+ 'url-icon-name' => '',
+ 'url-value' => '',
+ );
+
+ $args = wp_parse_args( $args, $defaults );
+ ?>
+
+ <div class="jetpack-social-icons-widget-item">
+ <div class="jetpack-social-icons-widget-item-wrapper">
+ <div class="handle"></div>
+
+ <p class="jetpack-widget-social-icons-url">
+ <?php
+ printf(
+ '<input class="widefat id="%1$s" name="%2$s[]" type="text" placeholder="%3$s" value="%4$s"/>',
+ esc_attr( $args['url-icon-id'] ),
+ esc_attr( $args['url-icon-name'] ),
+ esc_attr__( 'Account URL', 'jetpack' ),
+ esc_url( $args['url-value'], array( 'http', 'https', 'mailto', 'skype' ) )
+ );
+ ?>
+ </p>
+
+ <p class="jetpack-widget-social-icons-remove-item">
+ <a class="jetpack-widget-social-icons-remove-item-button" href="javascript:;">
+ <?php esc_html_e( 'Remove', 'jetpack' ); ?>
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <?php
+ }
+
+ /**
+ * Return SVG markup.
+ *
+ * @param array $args {
+ * Parameters needed to display an SVG.
+ *
+ * @type string $icon Required SVG icon filename.
+ * }
+ * @return string SVG markup.
+ */
+ public function get_svg_icon( $args = array() ) {
+ // Make sure $args are an array.
+ if ( empty( $args ) ) {
+ return esc_html__( 'Please define default parameters in the form of an array.', 'jetpack' );
+ }
+
+ // Set defaults.
+ $defaults = array(
+ 'icon' => '',
+ );
+
+ // Parse args.
+ $args = wp_parse_args( $args, $defaults );
+
+ // Define an icon.
+ if ( false === array_key_exists( 'icon', $args ) ) {
+ return esc_html__( 'Please define an SVG icon filename.', 'jetpack' );
+ }
+
+ // Set aria hidden.
+ $aria_hidden = ' aria-hidden="true"';
+
+ // Begin SVG markup.
+ $svg = '<svg class="icon icon-' . esc_attr( $args['icon'] ) . '"' . $aria_hidden . ' role="img">';
+
+ /*
+ * Display the icon.
+ *
+ * The whitespace around `<use>` is intentional - it is a work around to a keyboard navigation bug in Safari 10.
+ *
+ * See https://core.trac.wordpress.org/ticket/38387.
+ */
+ $svg .= ' <use href="#icon-' . esc_html( $args['icon'] ) . '" xlink:href="#icon-' . esc_html( $args['icon'] ) . '"></use> ';
+
+ $svg .= '</svg>';
+
+ return $svg;
+ }
+
+ /**
+ * Returns an array of supported social links (URL, icon, and label).
+ *
+ * @return array $social_links_icons
+ */
+ public function get_supported_icons() {
+ $social_links_icons = array(
+ array(
+ 'url' => '500px.com',
+ 'icon' => '500px',
+ 'label' => '500px',
+ ),
+ array(
+ 'url' => 'amazon.cn',
+ 'icon' => 'amazon',
+ 'label' => 'Amazon',
+ ),
+ array(
+ 'url' => 'amazon.in',
+ 'icon' => 'amazon',
+ 'label' => 'Amazon',
+ ),
+ array(
+ 'url' => 'amazon.fr',
+ 'icon' => 'amazon',
+ 'label' => 'Amazon',
+ ),
+ array(
+ 'url' => 'amazon.de',
+ 'icon' => 'amazon',
+ 'label' => 'Amazon',
+ ),
+ array(
+ 'url' => 'amazon.it',
+ 'icon' => 'amazon',
+ 'label' => 'Amazon',
+ ),
+ array(
+ 'url' => 'amazon.nl',
+ 'icon' => 'amazon',
+ 'label' => 'Amazon',
+ ),
+ array(
+ 'url' => 'amazon.es',
+ 'icon' => 'amazon',
+ 'label' => 'Amazon',
+ ),
+ array(
+ 'url' => 'amazon.co',
+ 'icon' => 'amazon',
+ 'label' => 'Amazon',
+ ),
+ array(
+ 'url' => 'amazon.ca',
+ 'icon' => 'amazon',
+ 'label' => 'Amazon',
+ ),
+ array(
+ 'url' => 'amazon.com',
+ 'icon' => 'amazon',
+ 'label' => 'Amazon',
+ ),
+ array(
+ 'url' => 'apple.com',
+ 'icon' => 'apple',
+ 'label' => 'Apple',
+ ),
+ array(
+ 'url' => 'itunes.com',
+ 'icon' => 'apple',
+ 'label' => 'iTunes',
+ ),
+ array(
+ 'url' => 'bandcamp.com',
+ 'icon' => 'bandcamp',
+ 'label' => 'Bandcamp',
+ ),
+ array(
+ 'url' => 'behance.net',
+ 'icon' => 'behance',
+ 'label' => 'Behance',
+ ),
+ array(
+ 'url' => 'codepen.io',
+ 'icon' => 'codepen',
+ 'label' => 'CodePen',
+ ),
+ array(
+ 'url' => 'deviantart.com',
+ 'icon' => 'deviantart',
+ 'label' => 'DeviantArt',
+ ),
+ array(
+ 'url' => 'digg.com',
+ 'icon' => 'digg',
+ 'label' => 'Digg',
+ ),
+ array(
+ 'url' => 'discord.gg',
+ 'icon' => 'discord',
+ 'label' => 'Discord',
+ ),
+ array(
+ 'url' => 'discordapp.com',
+ 'icon' => 'discord',
+ 'label' => 'Discord',
+ ),
+ array(
+ 'url' => 'dribbble.com',
+ 'icon' => 'dribbble',
+ 'label' => 'Dribbble',
+ ),
+ array(
+ 'url' => 'dropbox.com',
+ 'icon' => 'dropbox',
+ 'label' => 'Dropbox',
+ ),
+ array(
+ 'url' => 'etsy.com',
+ 'icon' => 'etsy',
+ 'label' => 'Etsy',
+ ),
+ array(
+ 'url' => 'facebook.com',
+ 'icon' => 'facebook',
+ 'label' => 'Facebook',
+ ),
+ array(
+ 'url' => '/feed/',
+ 'icon' => 'feed',
+ 'label' => __( 'RSS Feed', 'jetpack' ),
+ ),
+ array(
+ 'url' => 'flickr.com',
+ 'icon' => 'flickr',
+ 'label' => 'Flickr',
+ ),
+ array(
+ 'url' => 'foursquare.com',
+ 'icon' => 'foursquare',
+ 'label' => 'Foursquare',
+ ),
+ array(
+ 'url' => 'goodreads.com',
+ 'icon' => 'goodreads',
+ 'label' => 'Goodreads',
+ ),
+ array(
+ 'url' => 'google.com',
+ 'icon' => 'google',
+ 'label' => 'Google',
+ ),
+ array(
+ 'url' => 'github.com',
+ 'icon' => 'github',
+ 'label' => 'GitHub',
+ ),
+ array(
+ 'url' => 'instagram.com',
+ 'icon' => 'instagram',
+ 'label' => 'Instagram',
+ ),
+ array(
+ 'url' => 'linkedin.com',
+ 'icon' => 'linkedin',
+ 'label' => 'LinkedIn',
+ ),
+ array(
+ 'url' => 'mailto:',
+ 'icon' => 'mail',
+ 'label' => __( 'Email', 'jetpack' ),
+ ),
+ array(
+ 'url' => 'meetup.com',
+ 'icon' => 'meetup',
+ 'label' => 'Meetup',
+ ),
+ array(
+ 'url' => 'medium.com',
+ 'icon' => 'medium',
+ 'label' => 'Medium',
+ ),
+ array(
+ 'url' => 'pinterest.',
+ 'icon' => 'pinterest',
+ 'label' => 'Pinterest',
+ ),
+ array(
+ 'url' => 'getpocket.com',
+ 'icon' => 'pocket',
+ 'label' => 'Pocket',
+ ),
+ array(
+ 'url' => 'reddit.com',
+ 'icon' => 'reddit',
+ 'label' => 'Reddit',
+ ),
+ array(
+ 'url' => 'skype.com',
+ 'icon' => 'skype',
+ 'label' => 'Skype',
+ ),
+ array(
+ 'url' => 'skype:',
+ 'icon' => 'skype',
+ 'label' => 'Skype',
+ ),
+ array(
+ 'url' => 'slideshare.net',
+ 'icon' => 'slideshare',
+ 'label' => 'SlideShare',
+ ),
+ array(
+ 'url' => 'snapchat.com',
+ 'icon' => 'snapchat',
+ 'label' => 'Snapchat',
+ ),
+ array(
+ 'url' => 'soundcloud.com',
+ 'icon' => 'soundcloud',
+ 'label' => 'SoundCloud',
+ ),
+ array(
+ 'url' => 'spotify.com',
+ 'icon' => 'spotify',
+ 'label' => 'Spotify',
+ ),
+ array(
+ 'url' => 'stackoverflow.com',
+ 'icon' => 'stackoverflow',
+ 'label' => 'Stack Overflow',
+ ),
+ array(
+ 'url' => 'stumbleupon.com',
+ 'icon' => 'stumbleupon',
+ 'label' => 'StumbleUpon',
+ ),
+ array(
+ 'url' => 'tumblr.com',
+ 'icon' => 'tumblr',
+ 'label' => 'Tumblr',
+ ),
+ array(
+ 'url' => 'twitch.tv',
+ 'icon' => 'twitch',
+ 'label' => 'Twitch',
+ ),
+ array(
+ 'url' => 'twitter.com',
+ 'icon' => 'twitter',
+ 'label' => 'Twitter',
+ ),
+ array(
+ 'url' => 'vimeo.com',
+ 'icon' => 'vimeo',
+ 'label' => 'Vimeo',
+ ),
+ array(
+ 'url' => 'vk.com',
+ 'icon' => 'vk',
+ 'label' => 'VK',
+ ),
+ array(
+ 'url' => 'wordpress.com',
+ 'icon' => 'wordpress',
+ 'label' => 'WordPress.com',
+ ),
+ array(
+ 'url' => 'wordpress.org',
+ 'icon' => 'wordpress',
+ 'label' => 'WordPress',
+ ),
+ array(
+ 'url' => 'yelp.com',
+ 'icon' => 'yelp',
+ 'label' => 'Yelp',
+ ),
+ array(
+ 'url' => 'youtube.com',
+ 'icon' => 'youtube',
+ 'label' => 'YouTube',
+ ),
+ );
+
+ return $social_links_icons;
+ }
+} // Jetpack_Widget_Social_Icons
+
+/**
+ * Register and load the widget.
+ *
+ * @access public
+ * @return void
+ */
+function jetpack_widget_social_icons_load() {
+ register_widget( 'Jetpack_Widget_Social_Icons' );
+}
+add_action( 'widgets_init', 'jetpack_widget_social_icons_load' );
diff --git a/plugins/jetpack/modules/widgets/social-icons/social-icons-admin.css b/plugins/jetpack/modules/widgets/social-icons/social-icons-admin.css
new file mode 100644
index 00000000..575ac09f
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/social-icons/social-icons-admin.css
@@ -0,0 +1,94 @@
+.jetpack-social-icons-widget-item {
+ background: #fff;
+ border: 1px solid #e5e5e5;
+ cursor: move;
+ margin: 0;
+}
+
+html[class*='wordpress_com'] .jetpack-social-icons-widget-item,
+.in-calypso .jetpack-social-icons-widget-item {
+ border-color: #c8d7e1;
+}
+
+.jetpack-social-icons-widget-item:hover {
+ outline: 1px solid #999;
+ outline-offset: -1px;
+}
+
+html[class*='wordpress_com'] .jetpack-social-icons-widget-item:hover,
+.in-calypso .jetpack-social-icons-widget-item:hover {
+ outline-color: #a8bece;
+}
+
+.jetpack-social-icons-widget-item.ui-sortable-helper {
+ border-color: #999;
+}
+
+html[class*='wordpress_com'] .jetpack-social-icons-widget-item.ui-sortable-helper,
+.in-calypso .jetpack-social-icons-widget-item.ui-sortable-helper {
+ border-color: #a8bece;
+}
+
+.jetpack-social-icons-widget-item.ui-sortable-helper {
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.jetpack-social-icons-widget-item.ui-state-placeholder {
+ border-color: #a0a5aa;
+ border-style: dashed;
+ margin: 0 0 1px;
+}
+
+.jetpack-social-icons-widget-item + .jetpack-social-icons-widget-item:not(.ui-state-placeholder) {
+ margin-top: -1px;
+}
+
+.jetpack-social-icons-widget-item-wrapper {
+ padding: 1em;
+ position: relative;
+}
+
+.jetpack-social-icons-widget-item .handle {
+ display: block;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+
+.jetpack-social-icons-widget-item p {
+ margin: 0;
+ position: relative;
+}
+
+.jetpack-social-icons-widget-item p + p {
+ margin-top: 1em;
+}
+
+.jetpack-widget-social-icons-remove-item {
+ display: inline-block;
+}
+
+.jetpack-widget-social-icons-remove-item-button {
+ color: #a00;
+ text-decoration: none;
+}
+
+.jetpack-widget-social-icons-remove-item-button:focus,
+.jetpack-widget-social-icons-remove-item-button:hover {
+ color: #f00;
+}
+
+.jetpack-social-icons-add-button:before {
+ content: "\f132";
+ display: inline-block;
+ position: relative;
+ left: -2px;
+ top: -1px;
+ font: 400 20px/1 dashicons;
+ vertical-align: middle;
+ transition: all 0.2s;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
diff --git a/plugins/jetpack/modules/widgets/social-icons/social-icons-admin.js b/plugins/jetpack/modules/widgets/social-icons/social-icons-admin.js
new file mode 100644
index 00000000..d44716a9
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/social-icons/social-icons-admin.js
@@ -0,0 +1,144 @@
+( function( $ ) {
+ var timeout = null;
+
+ // Make the list of items sortable.
+ function initWidget( widget ) {
+ widget.find( '.jetpack-social-icons-widget-list' ).sortable( {
+ items: '> .jetpack-social-icons-widget-item',
+ handle: '.handle',
+ cursor: 'move',
+ placeholder: 'jetpack-social-icons-widget-item ui-state-placeholder',
+ containment: widget,
+ forcePlaceholderSize: true,
+ update: function() {
+ livePreviewUpdate(
+ $( this )
+ .parents( '.form' )
+ .find( '.widget-control-save' )
+ );
+ },
+ } );
+ }
+
+ // Live preview update.
+ function livePreviewUpdate( button ) {
+ if ( ! $( document.body ).hasClass( 'wp-customizer' ) || ! button.length ) {
+ return;
+ }
+
+ button.trigger( 'click' ).hide();
+ }
+
+ $( document ).ready( function() {
+ // Add an item.
+ $( document ).on( 'click', '.jetpack-social-icons-widget.add-button button', function( event ) {
+ event.preventDefault();
+
+ var template, widgetContent, widgetList, widgetLastItem, urlId, urlName;
+
+ template = $( $.trim( $( '#tmpl-jetpack-widget-social-icons-template' ).html() ) );
+ widgetContent = $( this ).parents( '.widget-content' );
+ widgetList = widgetContent.find( '.jetpack-social-icons-widget-list' );
+ urlId = widgetList.data( 'url-icon-id' );
+ urlName = widgetList.data( 'url-icon-name' );
+
+ template
+ .find( '.jetpack-widget-social-icons-url input' )
+ .attr( 'id', urlId )
+ .attr( 'name', urlName + '[]' );
+
+ widgetList.append( template );
+
+ widgetLastItem = widgetContent.find( '.jetpack-social-icons-widget-item:last' );
+ widgetLastItem.find( 'input:first' ).trigger( 'focus' );
+ } );
+
+ // Remove an item.
+ $( document ).on( 'click', '.jetpack-widget-social-icons-remove-item-button', function(
+ event
+ ) {
+ event.preventDefault();
+
+ var button = $( this )
+ .parents( '.form' )
+ .find( '.widget-control-save' );
+
+ $( this )
+ .parents( '.jetpack-social-icons-widget-item' )
+ .remove();
+
+ livePreviewUpdate( button );
+ } );
+
+ // Event handler for widget open button.
+ $( document ).on(
+ 'click',
+ 'div.widget[id*="jetpack_widget_social_icons"] .widget-title, div.widget[id*="jetpack_widget_social_icons"] .widget-action',
+ function() {
+ if ( $( this ).parents( '#available-widgets' ).length ) {
+ return;
+ }
+
+ initWidget( $( this ).parents( '.widget[id*="jetpack_widget_social_icons"]' ) );
+ }
+ );
+
+ // Event handler for widget added.
+ $( document ).on( 'widget-added', function( event, widget ) {
+ if ( widget.is( '[id*="jetpack_widget_social_icons"]' ) ) {
+ event.preventDefault();
+ initWidget( widget );
+ }
+ } );
+
+ // Event handler for widget updated.
+ $( document ).on( 'widget-updated', function( event, widget ) {
+ if ( widget.is( '[id*="jetpack_widget_social_icons"]' ) ) {
+ event.preventDefault();
+ initWidget( widget );
+ }
+ } );
+
+ // Live preview update on input focus out.
+ $( document ).on( 'focusout', 'input[name*="jetpack_widget_social_icons"]', function() {
+ livePreviewUpdate(
+ $( this )
+ .parents( '.form' )
+ .find( '.widget-control-save' )
+ );
+ } );
+
+ // Live preview update on input enter key.
+ $( document ).on( 'keydown', 'input[name*="jetpack_widget_social_icons"]', function( event ) {
+ if ( event.keyCode === 13 ) {
+ livePreviewUpdate(
+ $( this )
+ .parents( '.form' )
+ .find( '.widget-control-save' )
+ );
+ }
+ } );
+
+ // Live preview update on input key up 1s.
+ $( document ).on( 'keyup', 'input[name*="jetpack_widget_social_icons"]', function() {
+ clearTimeout( timeout );
+
+ timeout = setTimeout( function() {
+ livePreviewUpdate(
+ $( this )
+ .parents( '.form' )
+ .find( '.widget-control-save' )
+ );
+ }, 1000 );
+ } );
+
+ // Live preview update on select change.
+ $( document ).on( 'change', 'select[name*="jetpack_widget_social_icons"]', function() {
+ livePreviewUpdate(
+ $( this )
+ .parents( '.form' )
+ .find( '.widget-control-save' )
+ );
+ } );
+ } );
+} )( jQuery );
diff --git a/plugins/jetpack/modules/widgets/social-icons/social-icons.css b/plugins/jetpack/modules/widgets/social-icons/social-icons.css
new file mode 100644
index 00000000..505f0663
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/social-icons/social-icons.css
@@ -0,0 +1,75 @@
+.jetpack_widget_social_icons ul,
+.jetpack_widget_social_icons li {
+ list-style: none;
+}
+
+.jetpack_widget_social_icons ul {
+ display: block;
+ margin: 0 0 1.5em;
+ padding: 0;
+}
+
+.jetpack_widget_social_icons ul li {
+ border: 0;
+ display: inline-block;
+ line-height: 1;
+ margin: 0;
+ padding: 0;
+}
+
+.jetpack_widget_social_icons ul li:before,
+.jetpack_widget_social_icons ul li:after {
+ display: none;
+}
+
+.jetpack_widget_social_icons a {
+ border: 0;
+ box-shadow: none;
+ display: block;
+ height: 24px;
+ text-decoration: none;
+ width: 24px;
+}
+
+.jetpack_widget_social_icons svg {
+ color: inherit;
+ fill: currentColor;
+ height: inherit;
+ vertical-align: middle;
+ width: inherit;
+}
+
+/* Sizes */
+
+.jetpack_widget_social_icons ul.size-small a {
+ height: 24px;
+ width: 24px;
+}
+
+.jetpack_widget_social_icons ul.size-medium a {
+ height: 32px;
+ width: 32px;
+}
+
+.jetpack_widget_social_icons ul.size-large a {
+ height: 48px;
+ width: 48px;
+}
+
+/*
+Text meant only for screen readers.
+Provides support for themes that do not bundle this CSS yet.
+@see https://make.wordpress.org/accessibility/2015/02/09/hiding-text-for-screen-readers-with-wordpress-core/
+***********************************/
+.screen-reader-text {
+ border: 0;
+ clip: rect(1px, 1px, 1px, 1px);
+ clip-path: inset(50%);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute ! important;
+ width: 1px;
+ word-wrap: normal ! important;
+}
diff --git a/plugins/jetpack/modules/widgets/social-media-icons.php b/plugins/jetpack/modules/widgets/social-media-icons.php
new file mode 100644
index 00000000..0e8028ef
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/social-media-icons.php
@@ -0,0 +1,346 @@
+<?php
+/*
+Plugin Name: Social Media Icons Widget
+Description: A simple widget that displays social media icons
+Author: Automattic Inc.
+
+This widget is now deprecated.
+Any new features should go into modules/widgets/social-icons.php instead.
+@see https://github.com/Automattic/jetpack/pull/8498
+
+*/
+
+
+/**
+ * WPCOM_social_media_icons_widget class.
+ *
+ * @extends WP_Widget
+ */
+class WPCOM_social_media_icons_widget extends WP_Widget {
+
+ /**
+ * Defaults
+ *
+ * @var mixed
+ * @access private
+ */
+ private $defaults;
+
+ /**
+ * Services
+ *
+ * @var mixed
+ * @access private
+ */
+ private $services;
+
+
+ /**
+ * __construct function.
+ *
+ * @access public
+ * @return void
+ */
+ public function __construct() {
+ parent::__construct(
+ 'wpcom_social_media_icons_widget',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', esc_html__( 'Social Media Icons (Deprecated)', 'jetpack' ) ),
+ array(
+ 'description' => __( 'A simple widget that displays social media icons.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+ $this->defaults = array(
+ 'title' => __( 'Social', 'jetpack' ),
+ 'facebook_username' => '',
+ 'twitter_username' => '',
+ 'instagram_username' => '',
+ 'pinterest_username' => '',
+ 'linkedin_username' => '',
+ 'github_username' => '',
+ 'youtube_username' => '',
+ 'vimeo_username' => '',
+ 'googleplus_username' => '',
+ 'flickr_username' => '',
+ 'wordpress_username' => '',
+ 'twitch_username' => '',
+ 'tumblr_username' => '',
+ );
+ $this->services = array(
+ 'facebook' => array( 'Facebook', 'https://www.facebook.com/%s/' ),
+ 'twitter' => array( 'Twitter', 'https://twitter.com/%s/' ),
+ 'instagram' => array( 'Instagram', 'https://www.instagram.com/%s/' ),
+ 'pinterest' => array( 'Pinterest', 'https://www.pinterest.com/%s/' ),
+ 'linkedin' => array( 'LinkedIn', 'https://www.linkedin.com/in/%s/' ),
+ 'github' => array( 'GitHub', 'https://github.com/%s/' ),
+ 'youtube' => array( 'YouTube', 'https://www.youtube.com/%s/' ),
+ 'vimeo' => array( 'Vimeo', 'https://vimeo.com/%s/' ),
+ 'googleplus' => array( 'Google+', 'https://plus.google.com/u/0/%s/' ),
+ 'flickr' => array( 'Flickr', 'https://www.flickr.com/photos/%s/' ),
+ 'wordpress' => array( 'WordPress.org', 'https://profiles.wordpress.org/%s/' ),
+ 'twitch' => array( 'Twitch', 'https://www.twitch.tv/%s/' ),
+ 'tumblr' => array( 'Tumblr', 'https://%s.tumblr.com' ),
+ );
+ if ( is_active_widget( false, false, $this->id_base ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_style' ) );
+ }
+ }
+
+ /**
+ * Enqueue Style.
+ *
+ * @access public
+ * @return void
+ */
+ public function enqueue_style() {
+ wp_register_style( 'jetpack_social_media_icons_widget', plugins_url( 'social-media-icons/style.css', __FILE__ ), array(), '20150602' );
+ wp_enqueue_style( 'jetpack_social_media_icons_widget' );
+ }
+
+ /**
+ * Check Genericons.
+ *
+ * @access private
+ * @return Bool.
+ */
+ private function check_genericons() {
+ global $wp_styles;
+ foreach ( $wp_styles->queue as $handle ) {
+ if ( false !== stristr( $handle, 'genericons' ) ) {
+ return $handle;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Widget Front End.
+ *
+ * @access public
+ * @param mixed $args Arguments.
+ * @param mixed $instance Instance.
+ * @return void
+ */
+ public function widget( $args, $instance ) {
+ $instance = wp_parse_args( (array) $instance, $this->defaults );
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $instance['title'] = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
+ if ( ! $this->check_genericons() ) {
+ wp_enqueue_style( 'genericons' );
+ }
+ $index = 10;
+ $html = array();
+ $alt_text = esc_attr__( 'View %1$s&#8217;s profile on %2$s', 'jetpack' );
+ foreach ( $this->services as $service => $data ) {
+ list( $service_name, $url ) = $data;
+ if ( ! isset( $instance[ $service . '_username' ] ) ) {
+ continue;
+ }
+ $username = $link_username = $instance[ $service . '_username' ];
+ if ( empty( $username ) ) {
+ continue;
+ }
+ $index += 10;
+ $predefined_url = false;
+
+ /** Check if full URL entered in configuration, use it instead of tinkering **/
+ if (
+ in_array(
+ parse_url( $username, PHP_URL_SCHEME ),
+ array( 'http', 'https' )
+ )
+ ) {
+ $predefined_url = $username;
+
+ // In case of a predefined link we only display the service name
+ // for screen readers
+ $alt_text = '%2$s';
+ }
+
+ if ( 'googleplus' === $service
+ && ! is_numeric( $username )
+ && substr( $username, 0, 1 ) !== '+'
+ ) {
+ $link_username = '+' . $username;
+ }
+ if ( 'youtube' === $service && 'UC' === substr( $username, 0, 2 ) ) {
+ $link_username = 'channel/' . $username;
+ } elseif ( 'youtube' === $service ) {
+ $link_username = 'user/' . $username;
+ }
+
+ if ( ! $predefined_url ) {
+ $predefined_url = sprintf( $url, $link_username );
+ }
+ /**
+ * Fires for each profile link in the social icons widget. Can be used
+ * to change the links for certain social networks if needed. All URLs
+ * will be passed through `esc_attr` on output.
+ *
+ * @module widgets
+ *
+ * @since 3.8.0
+ *
+ * @param string $url the currently processed URL
+ * @param string $service the lowercase service slug, e.g. 'facebook', 'youtube', etc.
+ */
+ $link = apply_filters(
+ 'jetpack_social_media_icons_widget_profile_link',
+ $predefined_url,
+ $service
+ );
+ $html[ $index ] = sprintf(
+ '<a href="%1$s" class="genericon genericon-%2$s" target="_blank"><span class="screen-reader-text">%3$s</span></a>',
+ esc_attr( $link ),
+ esc_attr( $service ),
+ sprintf( $alt_text, esc_html( $username ), $service_name )
+ );
+ }
+ /**
+ * Fires at the end of the list of Social Media accounts.
+ * Can be used to add a new Social Media Site to the Social Media Icons Widget.
+ * The filter function passed the array of HTML entries that will be sorted
+ * by key, each wrapped in a list item element and output as an unsorted list.
+ *
+ * @module widgets
+ *
+ * @since 3.8.0
+ *
+ * @param array $html Associative array of HTML snippets per each icon.
+ */
+ $html = apply_filters( 'jetpack_social_media_icons_widget_array', $html );
+ ksort( $html );
+ $html = '<ul><li>' . join( '</li><li>', $html ) . '</li></ul>';
+ if ( ! empty( $instance['title'] ) ) {
+ $html = $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title'] . $html;
+ }
+ $html = $args['before_widget'] . $html . $args['after_widget'];
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'social_media_icons' );
+
+ /**
+ * Filters the Social Media Icons widget output.
+ *
+ * @module widgets
+ *
+ * @since 3.6.0
+ *
+ * @param string $html Social Media Icons widget html output.
+ */
+ echo apply_filters( 'jetpack_social_media_icons_widget_output', $html );
+ }
+
+ /**
+ * Widget Settings.
+ *
+ * @access public
+ * @param mixed $instance Instance.
+ * @return void
+ */
+ public function form( $instance ) {
+ $instance = wp_parse_args( (array) $instance, $this->defaults );
+ ?>
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_attr_e( 'Title:', 'jetpack' ); ?></label>
+ <input
+ class="widefat"
+ id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $instance['title'] ); ?>"
+ />
+ </p>
+ <?php
+ foreach ( $this->services as $service => $data ) {
+ list( $service_name, $url ) = $data;
+ ?>
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( $service . '_username' ) ); ?>">
+ <?php
+ /* translators: %s is a social network name, e.g. Facebook. */
+ printf( __( '%s username:', 'jetpack' ), $service_name );
+ ?>
+ </label>
+ <input
+ class="widefat"
+ id="<?php echo esc_attr( $this->get_field_id( $service . '_username' ) ); ?>"
+ name="<?php echo esc_attr( $this->get_field_name( $service . '_username' ) ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $instance[ $service . '_username' ] ); ?>"
+ />
+ </p>
+ <?php
+ }
+ }
+
+ /**
+ * Update Widget Settings.
+ *
+ * @access public
+ * @param mixed $new_instance New Instance.
+ * @param mixed $old_instance Old Instance.
+ * @return Instance.
+ */
+ public function update( $new_instance, $old_instance ) {
+ $instance = (array) $old_instance;
+ foreach ( $new_instance as $field => $value ) {
+ $instance[ $field ] = sanitize_text_field( $new_instance[ $field ] );
+ }
+ // Stats.
+ $stats = $instance;
+ unset( $stats['title'] );
+ $stats = array_filter( $stats );
+ $stats = array_keys( $stats );
+ $stats = array_map( array( $this, 'remove_username' ), $stats );
+ foreach ( $stats as $val ) {
+ /**
+ * Fires for each Social Media account being saved in the Social Media Widget settings.
+ *
+ * @module widgets
+ *
+ * @since 3.6.0
+ *
+ * @param string social-media-links-widget-svcs Type of action to track.
+ * @param string $val Name of the Social Media account being saved.
+ */
+ do_action( 'jetpack_bump_stats_extras', 'social-media-links-widget-svcs', $val );
+ }
+ return $instance;
+ }
+
+ /**
+ * Remove username from value before to save stats.
+ *
+ * @access public
+ * @param mixed $val Value.
+ * @return Value.
+ */
+ public function remove_username( $val ) {
+ return str_replace( '_username', '', $val );
+ }
+} // End Class.
+
+/**
+ * Register and load the widget.
+ *
+ * @access public
+ * @return void
+ */
+function wpcom_social_media_icons_widget_load_widget() {
+ $transient = 'wpcom_social_media_icons_widget::is_active';
+ $has_widget = get_transient( $transient );
+
+ if ( false === $has_widget ) {
+ $is_active_widget = is_active_widget( false, false, 'wpcom_social_media_icons_widget', false );
+ $has_widget = (int) ! empty( $is_active_widget );
+ set_transient( $transient, $has_widget, 1 * HOUR_IN_SECONDS );
+ }
+
+ // [DEPRECATION]: Only register widget if active widget exists already
+ if ( $has_widget ) {
+ register_widget( 'wpcom_social_media_icons_widget' );
+ }
+}
+add_action( 'widgets_init', 'wpcom_social_media_icons_widget_load_widget' );
diff --git a/plugins/jetpack/modules/widgets/social-media-icons/style.css b/plugins/jetpack/modules/widgets/social-media-icons/style.css
new file mode 100644
index 00000000..665d1c7d
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/social-media-icons/style.css
@@ -0,0 +1,49 @@
+.widget_wpcom_social_media_icons_widget ul {
+ list-style-type: none;
+ margin-left: 0;
+}
+
+.widget_wpcom_social_media_icons_widget ul li {
+ border: 0 none;
+ display: inline;
+ margin-right: 0.5em;
+}
+
+.widget_wpcom_social_media_icons_widget li a {
+ border: 0 none;
+ text-decoration: none;
+}
+
+.widget_wpcom_social_media_icons_widget .genericon {
+ font-family: 'Genericons';
+}
+
+.widget_wpcom_social_media_icons_widget .screen-reader-text {
+ clip: rect(1px, 1px, 1px, 1px);
+ position: absolute !important;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+}
+
+.widget_wpcom_social_media_icons_widget .screen-reader-text:hover,
+.widget_wpcom_social_media_icons_widget .screen-reader-text:active,
+.widget_wpcom_social_media_icons_widget .screen-reader-text:focus {
+ background-color: #f1f1f1;
+ border-radius: 3px;
+ box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);
+ clip: auto !important;
+ color: #21759b;
+ display: block;
+ font-size: 14px;
+ font-size: 0.875rem;
+ font-weight: bold;
+ height: auto;
+ left: 5px;
+ line-height: normal;
+ padding: 15px 23px 14px;
+ text-decoration: none;
+ top: 5px;
+ width: auto;
+ z-index: 100000; /* Above WP toolbar. */
+} \ No newline at end of file
diff --git a/plugins/jetpack/modules/widgets/top-posts.php b/plugins/jetpack/modules/widgets/top-posts.php
new file mode 100644
index 00000000..cdcd59d1
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/top-posts.php
@@ -0,0 +1,674 @@
+<?php
+
+/*
+ * Currently, this widget depends on the Stats Module. To not load this file
+ * when the Stats Module is not active would potentially bypass Jetpack's
+ * fatal error detection on module activation, so we always load this file.
+ * Instead, we don't register the widget if the Stats Module isn't active.
+ */
+
+/**
+ * Register the widget for use in Appearance -> Widgets
+ */
+add_action( 'widgets_init', 'jetpack_top_posts_widget_init' );
+
+function jetpack_top_posts_widget_init() {
+ // Currently, this widget depends on the Stats Module
+ if (
+ ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM )
+ &&
+ ! function_exists( 'stats_get_from_restapi' )
+ ) {
+ return;
+ }
+
+ register_widget( 'Jetpack_Top_Posts_Widget' );
+}
+
+class Jetpack_Top_Posts_Widget extends WP_Widget {
+ public $alt_option_name = 'widget_stats_topposts';
+ public $default_title = '';
+
+ function __construct() {
+ parent::__construct(
+ 'top-posts',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Top Posts &amp; Pages', 'jetpack' ) ),
+ array(
+ 'description' => __( 'Shows your most viewed posts and pages.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+
+ $this->default_title = __( 'Top Posts &amp; Pages', 'jetpack' );
+
+ if ( is_active_widget( false, false, $this->id_base ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_style' ) );
+ }
+
+ /**
+ * Add explanation about how the statistics are calculated.
+ *
+ * @module widgets
+ *
+ * @since 3.9.3
+ */
+ add_action( 'jetpack_widget_top_posts_after_fields', array( $this, 'stats_explanation' ) );
+ }
+
+ function enqueue_style() {
+ wp_register_style( 'jetpack-top-posts-widget', plugins_url( 'top-posts/style.css', __FILE__ ), array(), '20141013' );
+ wp_enqueue_style( 'jetpack-top-posts-widget' );
+ }
+
+ function form( $instance ) {
+ $instance = wp_parse_args( (array) $instance, $this->defaults() );
+
+ if ( false === $instance['title'] ) {
+ $instance['title'] = $this->default_title;
+ }
+ $title = stripslashes( $instance['title'] );
+
+ $count = isset( $instance['count'] ) ? (int) $instance['count'] : 10;
+ if ( $count < 1 || 10 < $count ) {
+ $count = 10;
+ }
+
+ $allowed_post_types = array_values( get_post_types( array( 'public' => true ) ) );
+ $types = isset( $instance['types'] ) ? (array) $instance['types'] : array( 'post', 'page' );
+
+ // 'likes' are not available in Jetpack
+ $ordering = isset( $instance['ordering'] ) && 'likes' === $instance['ordering'] ? 'likes' : 'views';
+
+ if ( isset( $instance['display'] ) && in_array( $instance['display'], array( 'grid', 'list', 'text' ) ) ) {
+ $display = $instance['display'];
+ } else {
+ $display = 'text';
+ }
+
+ ?>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php esc_html_e( 'Title:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'count' ); ?>"><?php esc_html_e( 'Maximum number of posts to show (no more than 10):', 'jetpack' ); ?></label>
+ <input id="<?php echo $this->get_field_id( 'count' ); ?>" name="<?php echo $this->get_field_name( 'count' ); ?>" type="number" value="<?php echo (int) $count; ?>" min="1" max="10" />
+ </p>
+
+ <?php if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) : ?>
+ <p>
+ <label><?php esc_html_e( 'Order Top Posts &amp; Pages By:', 'jetpack' ); ?></label>
+ <ul>
+ <li><label><input id="<?php echo $this->get_field_id( 'ordering' ); ?>-likes" name="<?php echo $this->get_field_name( 'ordering' ); ?>" type="radio" value="likes" <?php checked( 'likes', $ordering ); ?> /> <?php esc_html_e( 'Likes', 'jetpack' ); ?></label></li>
+ <li><label><input id="<?php echo $this->get_field_id( 'ordering' ); ?>-views" name="<?php echo $this->get_field_name( 'ordering' ); ?>" type="radio" value="views" <?php checked( 'views', $ordering ); ?> /> <?php esc_html_e( 'Views', 'jetpack' ); ?></label></li>
+ </ul>
+ </p>
+ <?php endif; ?>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'types' ); ?>"><?php esc_html_e( 'Types of pages to display:', 'jetpack' ); ?></label>
+ <ul>
+ <?php
+ foreach ( $allowed_post_types as $type ) {
+ // Get the Post Type name to display next to the checkbox
+ $post_type_object = get_post_type_object( $type );
+ $label = $post_type_object->labels->name;
+
+ $checked = '';
+ if ( in_array( $type, $types ) ) {
+ $checked = 'checked="checked" ';
+ }
+ ?>
+
+ <li><label>
+ <input value="<?php echo esc_attr( $type ); ?>" name="<?php echo $this->get_field_name( 'types' ); ?>[]" id="<?php echo $this->get_field_id( 'types' ); ?>-<?php echo $type; ?>" type="checkbox" <?php echo $checked; ?>>
+ <?php echo esc_html( $label ); ?>
+ </label></li>
+
+ <?php } // End foreach ?>
+ </ul>
+ </p>
+
+ <p>
+ <label><?php esc_html_e( 'Display as:', 'jetpack' ); ?></label>
+ <ul>
+ <li><label><input id="<?php echo $this->get_field_id( 'display' ); ?>-text" name="<?php echo $this->get_field_name( 'display' ); ?>" type="radio" value="text" <?php checked( 'text', $display ); ?> /> <?php esc_html_e( 'Text List', 'jetpack' ); ?></label></li>
+ <li><label><input id="<?php echo $this->get_field_id( 'display' ); ?>-list" name="<?php echo $this->get_field_name( 'display' ); ?>" type="radio" value="list" <?php checked( 'list', $display ); ?> /> <?php esc_html_e( 'Image List', 'jetpack' ); ?></label></li>
+ <li><label><input id="<?php echo $this->get_field_id( 'display' ); ?>-grid" name="<?php echo $this->get_field_name( 'display' ); ?>" type="radio" value="grid" <?php checked( 'grid', $display ); ?> /> <?php esc_html_e( 'Image Grid', 'jetpack' ); ?></label></li>
+ </ul>
+ </p>
+ <?php
+
+ /**
+ * Fires after the fields are displayed in the Top Posts Widget settings in wp-admin.
+ *
+ * Allow adding extra content after the fields are displayed.
+ *
+ * @module widgets
+ *
+ * @since 3.9.3
+ *
+ * @param array $args {
+ * @param array $instance The widget instance.
+ * @param object $this The class object.
+ * }
+ */
+ do_action( 'jetpack_widget_top_posts_after_fields', array( $instance, $this ) );
+ }
+
+ /**
+ * Explains how the statics are calculated.
+ */
+ function stats_explanation() {
+ ?>
+
+ <p><?php esc_html_e( 'Top Posts &amp; Pages by views are calculated from 24-48 hours of stats. They take a while to change.', 'jetpack' ); ?></p>
+ <?php
+ }
+
+ function update( $new_instance, $old_instance ) {
+ $instance = array();
+ $instance['title'] = wp_kses( $new_instance['title'], array() );
+ if ( $instance['title'] === $this->default_title ) {
+ $instance['title'] = false; // Store as false in case of language change
+ }
+
+ $instance['count'] = (int) $new_instance['count'];
+ if ( $instance['count'] < 1 || 10 < $instance['count'] ) {
+ $instance['count'] = 10;
+ }
+
+ // 'likes' are not available in Jetpack
+ $instance['ordering'] = isset( $new_instance['ordering'] ) && 'likes' == $new_instance['ordering'] ? 'likes' : 'views';
+
+ $allowed_post_types = array_values( get_post_types( array( 'public' => true ) ) );
+ $instance['types'] = $new_instance['types'];
+ foreach ( $new_instance['types'] as $key => $type ) {
+ if ( ! in_array( $type, $allowed_post_types ) ) {
+ unset( $new_instance['types'][ $key ] );
+ }
+ }
+
+ if ( isset( $new_instance['display'] ) && in_array( $new_instance['display'], array( 'grid', 'list', 'text' ) ) ) {
+ $instance['display'] = $new_instance['display'];
+ } else {
+ $instance['display'] = 'text';
+ }
+
+ /**
+ * Filters Top Posts Widget settings before they're saved.
+ *
+ * @module widgets
+ *
+ * @since 3.9.3
+ *
+ * @param array $instance The santized widget instance. Only contains data processed by the current widget.
+ * @param array $new_instance The new widget instance before sanitization.
+ */
+ $instance = apply_filters( 'jetpack_top_posts_saving', $instance, $new_instance );
+
+ return $instance;
+ }
+
+ function widget( $args, $instance ) {
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'top_posts' );
+
+ $instance = wp_parse_args( (array) $instance, $this->defaults() );
+
+ $title = isset( $instance['title'] ) ? $instance['title'] : false;
+ if ( false === $title ) {
+ $title = $this->default_title;
+ }
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', $title );
+
+ $count = isset( $instance['count'] ) ? (int) $instance['count'] : false;
+ if ( $count < 1 || 10 < $count ) {
+ $count = 10;
+ }
+ /**
+ * Control the number of displayed posts.
+ *
+ * @module widgets
+ *
+ * @since 3.3.0
+ *
+ * @param string $count Number of Posts displayed in the Top Posts widget. Default is 10.
+ */
+ $count = apply_filters( 'jetpack_top_posts_widget_count', $count );
+
+ $types = isset( $instance['types'] ) ? (array) $instance['types'] : array( 'post', 'page' );
+
+ // 'likes' are not available in Jetpack
+ $ordering = isset( $instance['ordering'] ) && 'likes' == $instance['ordering'] ? 'likes' : 'views';
+
+ if ( isset( $instance['display'] ) && in_array( $instance['display'], array( 'grid', 'list', 'text' ) ) ) {
+ $display = $instance['display'];
+ } else {
+ $display = 'text';
+ }
+
+ if ( 'text' != $display ) {
+ $get_image_options = array(
+ 'fallback_to_avatars' => true,
+ /** This filter is documented in modules/stats.php */
+ 'gravatar_default' => apply_filters( 'jetpack_static_url', set_url_scheme( 'https://en.wordpress.com/i/logo/white-gray-80.png' ) ),
+ 'avatar_size' => 40,
+ 'width' => null,
+ 'height' => null,
+ );
+ if ( 'grid' == $display ) {
+ $get_image_options['avatar_size'] = 200;
+ }
+ /**
+ * Top Posts Widget Image options.
+ *
+ * @module widgets
+ *
+ * @since 1.8.0
+ *
+ * @param array $get_image_options {
+ * Array of Image options.
+ * @type bool true Should we default to Gravatars when no image is found? Default is true.
+ * @type string $gravatar_default Default Image URL if no Gravatar is found.
+ * @type int $avatar_size Default Image size.
+ * @type mixed $width Image width, not set by default and $avatar_size is used instead.
+ * @type mixed $height Image height, not set by default and $avatar_size is used instead.
+ * }
+ */
+ $get_image_options = apply_filters( 'jetpack_top_posts_widget_image_options', $get_image_options );
+ }
+
+ if ( function_exists( 'wpl_get_blogs_most_liked_posts' ) && 'likes' == $ordering ) {
+ $posts = $this->get_by_likes( $count );
+ } else {
+ $posts = $this->get_by_views( $count, $args );
+ }
+
+ // Filter the returned posts. Remove all posts that do not match the chosen Post Types.
+ if ( isset( $types ) ) {
+ foreach ( $posts as $k => $post ) {
+ if ( ! in_array( $post['post_type'], $types ) ) {
+ unset( $posts[ $k ] );
+ }
+ }
+ }
+
+ if ( ! $posts ) {
+ $posts = $this->get_fallback_posts();
+ }
+
+ echo $args['before_widget'];
+ if ( ! empty( $title ) ) {
+ echo $args['before_title'] . $title . $args['after_title'];
+ }
+
+ if ( ! $posts ) {
+ $link = 'https://jetpack.com/support/getting-more-views-and-traffic/';
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $link = 'http://en.support.wordpress.com/getting-more-site-traffic/';
+ }
+
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ echo '<p>' . sprintf(
+ __( 'There are no posts to display. <a href="%s" target="_blank">Want more traffic?</a>', 'jetpack' ),
+ esc_url( $link )
+ ) . '</p>';
+ }
+
+ echo $args['after_widget'];
+ return;
+ }
+
+ /**
+ * Filter the layout of the Top Posts Widget
+ *
+ * @module widgets
+ *
+ * @since 6.4.0
+ *
+ * @param string $layout layout of the Top Posts Widget (empty string)
+ * @param array $posts IDs of the posts to be displayed
+ * @param array $display Display option from widget form
+ */
+ $layout = apply_filters( 'jetpack_top_posts_widget_layout', '', $posts, $display );
+ if ( ! empty( $layout ) ) {
+ echo $layout;
+ echo $args['after_widget'];
+ return;
+ }
+
+ switch ( $display ) {
+ case 'list':
+ case 'grid':
+ // Keep the avatar_size as default dimensions for backward compatibility.
+ $width = (int) $get_image_options['avatar_size'];
+ $height = (int) $get_image_options['avatar_size'];
+
+ // Check if the user has changed the width.
+ if ( ! empty( $get_image_options['width'] ) ) {
+ $width = (int) $get_image_options['width'];
+ }
+
+ // Check if the user has changed the height.
+ if ( ! empty( $get_image_options['height'] ) ) {
+ $height = (int) $get_image_options['height'];
+ }
+
+ foreach ( $posts as &$post ) {
+ $image = Jetpack_PostImages::get_image(
+ $post['post_id'],
+ array(
+ 'fallback_to_avatars' => true,
+ 'width' => (int) $width,
+ 'height' => (int) $height,
+ 'avatar_size' => (int) $get_image_options['avatar_size'],
+ )
+ );
+ $post['image'] = $image['src'];
+ if ( 'blavatar' != $image['from'] && 'gravatar' != $image['from'] ) {
+ $post['image'] = jetpack_photon_url( $post['image'], array( 'resize' => "$width,$height" ) );
+ }
+ }
+
+ unset( $post );
+
+ if ( 'grid' == $display ) {
+ echo "<div class='widgets-grid-layout no-grav'>\n";
+ foreach ( $posts as $post ) :
+ ?>
+ <div class="widget-grid-view-image">
+ <?php
+ /**
+ * Fires before each Top Post result, inside <li>.
+ *
+ * @module widgets
+ *
+ * @since 3.2.0
+ *
+ * @param string $post['post_id'] Post ID.
+ */
+ do_action( 'jetpack_widget_top_posts_before_post', $post['post_id'] );
+
+ /**
+ * Filter the permalink of items in the Top Posts widget.
+ *
+ * @module widgets
+ *
+ * @since 4.4.0
+ *
+ * @param string $post['permalink'] Post permalink.
+ * @param array $post Post array.
+ */
+ $filtered_permalink = apply_filters( 'jetpack_top_posts_widget_permalink', $post['permalink'], $post );
+
+ ?>
+ <a href="<?php echo esc_url( $filtered_permalink ); ?>" title="<?php echo esc_attr( wp_kses( $post['title'], array() ) ); ?>" class="bump-view" data-bump-view="tp">
+ <img width="<?php echo absint( $width ); ?>" height="<?php echo absint( $height ); ?>" src="<?php echo esc_url( $post['image'] ); ?>" alt="<?php echo esc_attr( wp_kses( $post['title'], array() ) ); ?>" data-pin-nopin="true" />
+ </a>
+ <?php
+ /**
+ * Fires after each Top Post result, inside <li>.
+ *
+ * @module widgets
+ *
+ * @since 3.2.0
+ *
+ * @param string $post['post_id'] Post ID.
+ */
+ do_action( 'jetpack_widget_top_posts_after_post', $post['post_id'] );
+ ?>
+ </div>
+ <?php
+ endforeach;
+ echo "</div>\n";
+ } else {
+ echo "<ul class='widgets-list-layout no-grav'>\n";
+ foreach ( $posts as $post ) :
+ ?>
+ <li>
+ <?php
+ /** This action is documented in modules/widgets/top-posts.php */
+ do_action( 'jetpack_widget_top_posts_before_post', $post['post_id'] );
+
+ /** This filter is documented in modules/widgets/top-posts.php */
+ $filtered_permalink = apply_filters( 'jetpack_top_posts_widget_permalink', $post['permalink'], $post );
+ ?>
+ <a href="<?php echo esc_url( $filtered_permalink ); ?>" title="<?php echo esc_attr( wp_kses( $post['title'], array() ) ); ?>" class="bump-view" data-bump-view="tp">
+ <img width="<?php echo absint( $width ); ?>" height="<?php echo absint( $height ); ?>" src="<?php echo esc_url( $post['image'] ); ?>" class='widgets-list-layout-blavatar' alt="<?php echo esc_attr( wp_kses( $post['title'], array() ) ); ?>" data-pin-nopin="true" />
+ </a>
+ <div class="widgets-list-layout-links">
+ <a href="<?php echo esc_url( $filtered_permalink ); ?>" class="bump-view" data-bump-view="tp">
+ <?php echo esc_html( wp_kses( $post['title'], array() ) ); ?>
+ </a>
+ </div>
+ <?php
+ /** This action is documented in modules/widgets/top-posts.php */
+ do_action( 'jetpack_widget_top_posts_after_post', $post['post_id'] );
+ ?>
+ </li>
+ <?php
+ endforeach;
+ echo "</ul>\n";
+ }
+ break;
+ default:
+ echo '<ul>';
+ foreach ( $posts as $post ) :
+ ?>
+ <li>
+ <?php
+ /** This action is documented in modules/widgets/top-posts.php */
+ do_action( 'jetpack_widget_top_posts_before_post', $post['post_id'] );
+
+ /** This filter is documented in modules/widgets/top-posts.php */
+ $filtered_permalink = apply_filters( 'jetpack_top_posts_widget_permalink', $post['permalink'], $post );
+ ?>
+ <a href="<?php echo esc_url( $filtered_permalink ); ?>" class="bump-view" data-bump-view="tp">
+ <?php echo esc_html( wp_kses( $post['title'], array() ) ); ?>
+ </a>
+ <?php
+ /** This action is documented in modules/widgets/top-posts.php */
+ do_action( 'jetpack_widget_top_posts_after_post', $post['post_id'] );
+ ?>
+ </li>
+ <?php
+ endforeach;
+ echo '</ul>';
+ }
+
+ echo $args['after_widget'];
+ }
+
+ public static function defaults() {
+ return array(
+ 'title' => esc_html__( 'Top Posts &amp; Pages', 'jetpack' ),
+ 'count' => absint( 10 ),
+ 'types' => array( 'post', 'page' ),
+ 'ordering' => 'views',
+ 'display' => 'text',
+ );
+ }
+
+ /*
+ * Get most liked posts
+ *
+ * ONLY TO BE USED IN WPCOM
+ */
+ function get_by_likes( $count ) {
+ $post_likes = wpl_get_blogs_most_liked_posts();
+ if ( ! $post_likes ) {
+ return array();
+ }
+
+ return $this->get_posts( array_keys( $post_likes ), $count );
+ }
+
+ function get_by_views( $count, $args ) {
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ global $wpdb;
+
+ $post_views = wp_cache_get( "get_top_posts_$count", 'stats' );
+ if ( false === $post_views ) {
+ $post_views = array_shift( stats_get_daily_history( false, get_current_blog_id(), 'postviews', 'post_id', false, 2, '', $count * 2 + 10, true ) );
+ unset( $post_views[0] );
+ wp_cache_add( "get_top_posts_$count", $post_views, 'stats', 1200 );
+ }
+
+ return $this->get_posts( array_keys( $post_views ), $count );
+ }
+
+ /**
+ * Filter the number of days used to calculate Top Posts for the Top Posts widget.
+ * We do not recommend accessing more than 10 days of results at one.
+ * When more than 10 days of results are accessed at once, results should be cached via the WordPress transients API.
+ * Querying for -1 days will give results for an infinite number of days.
+ *
+ * @module widgets
+ *
+ * @since 3.9.3
+ *
+ * @param int 2 Number of days. Default is 2.
+ * @param array $args The widget arguments.
+ */
+ $days = (int) apply_filters( 'jetpack_top_posts_days', 2, $args );
+
+ /** Handling situations where the number of days makes no sense - allows for unlimited days where $days = -1 */
+ if ( 0 == $days || false == $days ) {
+ $days = 2;
+ }
+
+ $post_view_posts = stats_get_from_restapi( array(), 'top-posts?max=11&summarize=1&num=' . intval( $days ) );
+
+ if ( ! isset( $post_view_posts->summary ) || empty( $post_view_posts->summary->postviews ) ) {
+ return array();
+ }
+
+ $post_view_ids = array_filter( wp_list_pluck( $post_view_posts->summary->postviews, 'id' ) );
+
+ if ( ! $post_view_ids ) {
+ return array();
+ }
+
+ return $this->get_posts( $post_view_ids, $count );
+ }
+
+ function get_fallback_posts() {
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ return array();
+ }
+
+ $post_query = new WP_Query;
+
+ $posts = $post_query->query(
+ array(
+ 'posts_per_page' => 1,
+ 'post_status' => 'publish',
+ 'post_type' => array( 'post', 'page' ),
+ 'no_found_rows' => true,
+ )
+ );
+
+ if ( ! $posts ) {
+ return array();
+ }
+
+ $post = array_pop( $posts );
+
+ return $this->get_posts( $post->ID, 1 );
+ }
+
+ function get_posts( $post_ids, $count ) {
+ $counter = 0;
+
+ $posts = array();
+ foreach ( (array) $post_ids as $post_id ) {
+ $post = get_post( $post_id );
+
+ if ( ! $post ) {
+ continue;
+ }
+
+ /**
+ * Attachment pages use the 'inherit' post status by default.
+ * To be able to remove attachment pages from private and password protect posts,
+ * we need to replace their post status by the parent post' status.
+ */
+ if ( 'inherit' == $post->post_status && 'attachment' == $post->post_type ) {
+ $post->post_status = get_post_status( $post_id );
+ }
+
+ // hide private and password protected posts
+ if ( 'publish' != $post->post_status || ! empty( $post->post_password ) ) {
+ continue;
+ }
+
+ // Both get HTML stripped etc on display
+ if ( empty( $post->post_title ) ) {
+ $title_source = $post->post_content;
+ $title = wp_html_excerpt( $title_source, 50 );
+ $title .= '&hellip;';
+ } else {
+ $title = $post->post_title;
+ }
+
+ $permalink = get_permalink( $post->ID );
+
+ $post_type = $post->post_type;
+
+ $posts[] = compact( 'title', 'permalink', 'post_id', 'post_type' );
+ $counter++;
+
+ if ( $counter == $count ) {
+ break; // only need to load and show x number of likes
+ }
+ }
+
+ /**
+ * Filter the Top Posts and Pages.
+ *
+ * @module widgets
+ *
+ * @since 3.0.0
+ *
+ * @param array $posts Array of the most popular posts.
+ * @param array $post_ids Array of Post IDs.
+ * @param string $count Number of Top Posts we want to display.
+ */
+ return apply_filters( 'jetpack_widget_get_top_posts', $posts, $post_ids, $count );
+ }
+}
+
+/**
+ * Create a shortcode to display the widget anywhere.
+ *
+ * @since 3.9.2
+ */
+function jetpack_do_top_posts_widget( $instance ) {
+ // Post Types can't be entered as an array in the shortcode parameters.
+ if ( isset( $instance['types'] ) && is_array( $instance['types'] ) ) {
+ $instance['types'] = implode( ',', $instance['types'] );
+ }
+
+ $instance = shortcode_atts(
+ Jetpack_Top_Posts_Widget::defaults(),
+ $instance,
+ 'jetpack_top_posts_widget'
+ );
+
+ // Add a class to allow styling
+ $args = array(
+ 'before_widget' => sprintf( '<div class="%s">', 'jetpack_top_posts_widget' ),
+ );
+
+ ob_start();
+ the_widget( 'Jetpack_Top_Posts_Widget', $instance, $args );
+ $output = ob_get_clean();
+
+ return $output;
+}
+add_shortcode( 'jetpack_top_posts_widget', 'jetpack_do_top_posts_widget' );
diff --git a/plugins/jetpack/modules/widgets/top-posts/style.css b/plugins/jetpack/modules/widgets/top-posts/style.css
new file mode 100644
index 00000000..ceeae12b
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/top-posts/style.css
@@ -0,0 +1,114 @@
+/*
+ * Top Posts Widget styles for Jetpack
+ */
+
+/* 2-Column Grid Layout */
+
+.widgets-grid-layout {
+ width: 100%;
+}
+
+.widgets-grid-layout:before,
+.widgets-grid-layout:after {
+ content: " ";
+ display: table;
+}
+
+.widgets-grid-layout:after {
+ clear: both;
+}
+
+.widget-grid-view-image {
+ float: left;
+ max-width: 50%;
+}
+
+.widget-grid-view-image a {
+ display: block;
+ margin: 0 2px 4px 0;
+}
+
+.widget-grid-view-image:nth-child(even) {
+ float: right;
+}
+
+.widget-grid-view-image:nth-child(even) a {
+ margin: 0 0 4px 2px;
+}
+
+.widgets-grid-layout .widget-grid-view-image img {
+ max-width: 100%;
+ height: auto;
+}
+
+/* Multi-Column Grid Layout */
+
+.widgets-multi-column-grid ul {
+ overflow: hidden;
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+}
+
+.widgets-multi-column-grid ul li {
+ background: none;
+ clear: none;
+ float: left;
+ margin: 0 -5px -3px 0;
+ padding: 0 8px 6px 0;
+ border: none;
+ list-style-type: none !important;
+}
+
+.widgets-multi-column-grid ul li a {
+ background: none;
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+
+.widgets-multi-column-grid .avatar {
+ vertical-align: middle;
+}
+
+/* List Layout */
+
+.widgets-list-layout {
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+}
+
+.widgets-list-layout li:before,
+.widgets-list-layout li:after {
+ content:"";
+ display:table;
+}
+.widgets-list-layout li:after {
+ clear:both;
+}
+.widgets-list-layout li {
+ zoom:1;
+ margin-bottom: 1em;
+ list-style-type: none !important;
+}
+
+.widgets-list-layout .widgets-list-layout-blavatar {
+ float: left;
+ width: 21.276596%;
+ max-width: 40px;
+ height: auto;
+}
+
+.widgets-list-layout-links {
+ float: right;
+ width: 73.404255%;
+}
+
+.widgets-list-layout span {
+ opacity: 0.5;
+}
+
+.widgets-list-layout span:hover {
+ opacity: 0.8;
+}
diff --git a/plugins/jetpack/modules/widgets/twitter-timeline-admin.js b/plugins/jetpack/modules/widgets/twitter-timeline-admin.js
new file mode 100644
index 00000000..e6a65140
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/twitter-timeline-admin.js
@@ -0,0 +1,35 @@
+jQuery( function( $ ) {
+ function twitterWidgetTypeChanged( widgetTypeSelector ) {
+ var selectedType = $( widgetTypeSelector ).val();
+ $( widgetTypeSelector )
+ .closest( '.jetpack-twitter-timeline-widget-type-container' )
+ .next( '.jetpack-twitter-timeline-widget-id-container' )
+ .find( 'label' )
+ .css( 'display', function() {
+ var labelType = $( this ).data( 'widget-type' );
+ if ( selectedType === labelType ) {
+ return '';
+ } else {
+ return 'none';
+ }
+ } );
+ }
+
+ // We could either be in wp-admin/widgets.php or the Customizer.
+ var $container = $( '#customize-controls' );
+ if ( ! $container.length ) {
+ $container = $( '#wpbody' );
+ }
+
+ // Observe widget settings for 'change' events of the 'type' property for
+ // current and future Twitter timeline widgets.
+ $container.on( 'change', '.jetpack-twitter-timeline-widget-type', function() {
+ twitterWidgetTypeChanged( this );
+ } );
+
+ // Set the labels for currently existing widgets (including the "template"
+ // version that is copied when a new widget is added).
+ $container.find( '.jetpack-twitter-timeline-widget-type' ).each( function() {
+ twitterWidgetTypeChanged( this );
+ } );
+} );
diff --git a/plugins/jetpack/modules/widgets/twitter-timeline.php b/plugins/jetpack/modules/widgets/twitter-timeline.php
new file mode 100644
index 00000000..0f16e330
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/twitter-timeline.php
@@ -0,0 +1,503 @@
+<?php
+
+/*
+ * Based on Evolution Twitter Timeline
+ * (https://wordpress.org/extend/plugins/evolution-twitter-timeline/)
+ * For details on Twitter Timelines see:
+ * - https://twitter.com/settings/widgets
+ * - https://dev.twitter.com/docs/embedded-timelines
+ */
+
+/**
+ * Register the widget for use in Appearance -> Widgets
+ */
+add_action( 'widgets_init', 'jetpack_twitter_timeline_widget_init' );
+
+function jetpack_twitter_timeline_widget_init() {
+ register_widget( 'Jetpack_Twitter_Timeline_Widget' );
+}
+
+class Jetpack_Twitter_Timeline_Widget extends WP_Widget {
+ /**
+ * Register widget with WordPress.
+ */
+ public function __construct() {
+ parent::__construct(
+ 'twitter_timeline',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', esc_html__( 'Twitter Timeline', 'jetpack' ) ),
+ array(
+ 'classname' => 'widget_twitter_timeline',
+ 'description' => __( 'Display an official Twitter Embedded Timeline widget.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+
+ if ( is_active_widget( false, false, $this->id_base ) || is_active_widget( false, false, 'monster' ) || is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+ }
+
+ add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
+ }
+
+ /**
+ * Enqueue scripts.
+ */
+ public function enqueue_scripts() {
+ wp_enqueue_script( 'jetpack-twitter-timeline' );
+ }
+
+ /**
+ * Enqueue Twitter's widget library.
+ *
+ * @deprecated
+ */
+ public function library() {
+ _deprecated_function( __METHOD__, '4.0.0' );
+ wp_print_scripts( array( 'jetpack-twitter-timeline' ) );
+ }
+
+ /**
+ * Enqueue script to improve admin UI
+ */
+ public function admin_scripts( $hook ) {
+ // This is still 'widgets.php' when managing widgets via the Customizer.
+ if ( 'widgets.php' === $hook ) {
+ wp_enqueue_script(
+ 'twitter-timeline-admin',
+ Jetpack::get_file_url_for_environment(
+ '_inc/build/widgets/twitter-timeline-admin.min.js',
+ 'modules/widgets/twitter-timeline-admin.js'
+ )
+ );
+ }
+ }
+
+ /**
+ * Front-end display of widget.
+ *
+ * @see WP_Widget::widget()
+ *
+ * @param array $args Widget arguments.
+ * @param array $instance Saved values from database.
+ */
+ public function widget( $args, $instance ) {
+ // Twitter deprecated `data-widget-id` on 2018-05-25,
+ // with cease support deadline on 2018-07-27.
+ if ( isset( $instance['type'] ) && 'widget-id' === $instance['type'] ) {
+ if ( current_user_can( 'edit_theme_options' ) ) {
+ echo $args['before_widget'];
+ echo $args['before_title'] . esc_html__( 'Twitter Timeline', 'jetpack' ) . $args['after_title'];
+ echo '<p>' . esc_html__( "The Twitter Timeline widget can't display tweets based on searches or hashtags. To display a simple list of tweets instead, change the Widget ID to a Twitter username. Otherwise, delete this widget.", 'jetpack' ) . '</p>';
+ echo '<p>' . esc_html__( '(Only administrators will see this message.)', 'jetpack' ) . '</p>';
+ echo $args['after_widget'];
+ }
+ return;
+ }
+
+ $instance['lang'] = substr( strtoupper( get_locale() ), 0, 2 );
+
+ echo $args['before_widget'];
+
+ $title = isset( $instance['title'] ) ? $instance['title'] : '';
+
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $title = apply_filters( 'widget_title', $title );
+ if ( ! empty( $title ) ) {
+ echo $args['before_title'] . $title . $args['after_title'];
+ }
+
+ if ( isset( $instance['type'] ) && 'widget-id' === $instance['type'] && current_user_can( 'edit_theme_options' ) ) {
+ echo '<p>' . esc_html__( 'As of July 27, 2018, the Twitter Timeline widget will no longer display tweets based on searches or hashtags. To display a simple list of tweets instead, change the Widget ID to a Twitter username.', 'jetpack' ) . '</p>';
+ echo '<p>' . esc_html__( '(Only administrators will see this message.)', 'jetpack' ) . '</p>';
+ }
+
+ // Start tag output
+ // This tag is transformed into the widget markup by Twitter's
+ // widgets.js code
+ echo '<a class="twitter-timeline"';
+
+ $data_attribs = array(
+ 'width',
+ 'height',
+ 'theme',
+ 'link-color',
+ 'border-color',
+ 'tweet-limit',
+ 'lang',
+ );
+ foreach ( $data_attribs as $att ) {
+ if ( ! empty( $instance[ $att ] ) && ! is_array( $instance[ $att ] ) ) {
+ echo ' data-' . esc_attr( $att ) . '="' . esc_attr( $instance[ $att ] ) . '"';
+ }
+ }
+
+ /** This filter is documented in modules/shortcodes/tweet.php */
+ $partner = apply_filters( 'jetpack_twitter_partner_id', 'jetpack' );
+ if ( ! empty( $partner ) ) {
+ echo ' data-partner="' . esc_attr( $partner ) . '"';
+ }
+
+ /**
+ * Allow the activation of Do Not Track for the Twitter Timeline Widget.
+ *
+ * @see https://developer.twitter.com/en/docs/twitter-for-websites/timelines/guides/parameter-reference.html
+ *
+ * @module widgets
+ *
+ * @since 6.9.0
+ *
+ * @param bool false Should the Twitter Timeline use the DNT attribute? Default to false.
+ */
+ $dnt = apply_filters( 'jetpack_twitter_timeline_default_dnt', false );
+ if ( true === $dnt ) {
+ echo ' data-dnt="true"';
+ }
+
+ if ( ! empty( $instance['chrome'] ) && is_array( $instance['chrome'] ) ) {
+ echo ' data-chrome="' . esc_attr( join( ' ', $instance['chrome'] ) ) . '"';
+ }
+
+ $type = ( isset( $instance['type'] ) ? $instance['type'] : '' );
+ $widget_id = ( isset( $instance['widget-id'] ) ? $instance['widget-id'] : '' );
+ switch ( $type ) {
+ case 'profile':
+ echo ' href="https://twitter.com/' . esc_attr( $widget_id ) . '"';
+ break;
+ case 'widget-id':
+ default:
+ echo ' data-widget-id="' . esc_attr( $widget_id ) . '"';
+ break;
+ }
+ echo ' href="https://twitter.com/' . esc_attr( $widget_id ) . '"';
+
+ // End tag output
+ echo '>';
+
+ $timeline_placeholder = __( 'My Tweets', 'jetpack' );
+
+ /**
+ * Filter the Timeline placeholder text.
+ *
+ * @module widgets
+ *
+ * @since 3.4.0
+ *
+ * @param string $timeline_placeholder Timeline placeholder text.
+ */
+ $timeline_placeholder = apply_filters( 'jetpack_twitter_timeline_placeholder', $timeline_placeholder );
+
+ echo esc_html( $timeline_placeholder ) . '</a>';
+
+ // End tag output
+
+ echo $args['after_widget'];
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'twitter_timeline' );
+ }
+
+
+ /**
+ * Sanitize widget form values as they are saved.
+ *
+ * @see WP_Widget::update()
+ *
+ * @param array $new_instance Values just sent to be saved.
+ * @param array $old_instance Previously saved values from database.
+ *
+ * @return array Updated safe values to be saved.
+ */
+ public function update( $new_instance, $old_instance ) {
+ $instance = array();
+
+ $instance['title'] = sanitize_text_field( $new_instance['title'] );
+
+ $width = (int) $new_instance['width'];
+ if ( $width ) {
+ // From publish.twitter.com: 220 <= width <= 1200
+ $instance['width'] = min( max( $width, 220 ), 1200 );
+ } else {
+ $instance['width'] = '';
+ }
+
+ $height = (int) $new_instance['height'];
+ if ( $height ) {
+ // From publish.twitter.com: height >= 200
+ $instance['height'] = max( $height, 200 );
+ } else {
+ $instance['height'] = '';
+ }
+
+ $tweet_limit = (int) $new_instance['tweet-limit'];
+ if ( $tweet_limit ) {
+ $instance['tweet-limit'] = min( max( $tweet_limit, 1 ), 20 );
+ /**
+ * A timeline with a specified limit is expanded to the height of those Tweets.
+ * The specified height value no longer applies, so reject the height value
+ * when a valid limit is set: a widget attempting to save both limit 5 and
+ * height 400 would be saved with just limit 5.
+ */
+ $instance['height'] = '';
+ } else {
+ $instance['tweet-limit'] = null;
+ }
+
+ // If they entered something that might be a full URL, try to parse it out
+ if ( is_string( $new_instance['widget-id'] ) ) {
+ if ( preg_match(
+ '#https?://twitter\.com/settings/widgets/(\d+)#s',
+ $new_instance['widget-id'],
+ $matches
+ ) ) {
+ $new_instance['widget-id'] = $matches[1];
+ }
+ }
+
+ $instance['widget-id'] = sanitize_text_field( $new_instance['widget-id'] );
+
+ $hex_regex = '/#([a-f]|[A-F]|[0-9]){3}(([a-f]|[A-F]|[0-9]){3})?\b/';
+ foreach ( array( 'link-color', 'border-color' ) as $color ) {
+ $new_color = sanitize_text_field( $new_instance[ $color ] );
+ if ( preg_match( $hex_regex, $new_color ) ) {
+ $instance[ $color ] = $new_color;
+ }
+ }
+
+ $instance['type'] = 'profile';
+
+ $instance['theme'] = 'light';
+ if ( in_array( $new_instance['theme'], array( 'light', 'dark' ) ) ) {
+ $instance['theme'] = $new_instance['theme'];
+ }
+
+ $instance['chrome'] = array();
+ $chrome_settings = array(
+ 'noheader',
+ 'nofooter',
+ 'noborders',
+ 'transparent',
+ 'noscrollbar',
+ );
+ if ( isset( $new_instance['chrome'] ) ) {
+ foreach ( $new_instance['chrome'] as $chrome ) {
+ if ( in_array( $chrome, $chrome_settings ) ) {
+ $instance['chrome'][] = $chrome;
+ }
+ }
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Returns a link to the documentation for a feature of this widget on
+ * Jetpack or WordPress.com.
+ */
+ public function get_docs_link( $hash = '' ) {
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $base_url = 'https://support.wordpress.com/widgets/twitter-timeline-widget/';
+ } else {
+ $base_url = 'https://jetpack.com/support/extra-sidebar-widgets/twitter-timeline-widget/';
+ }
+ return '<a href="' . $base_url . $hash . '" target="_blank">( ? )</a>';
+ }
+
+ /**
+ * Back end widget form.
+ *
+ * @see WP_Widget::form()
+ *
+ * @param array $instance Previously saved values from database.
+ */
+ public function form( $instance ) {
+ $defaults = array(
+ 'title' => esc_html__( 'Follow me on Twitter', 'jetpack' ),
+ 'width' => '',
+ 'height' => '400',
+ 'type' => 'profile',
+ 'widget-id' => '',
+ 'link-color' => '#f96e5b',
+ 'border-color' => '#e8e8e8',
+ 'theme' => 'light',
+ 'chrome' => array(),
+ 'tweet-limit' => null,
+ );
+
+ $instance = wp_parse_args( (array) $instance, $defaults );
+
+ if ( 'widget-id' === $instance['type'] ) {
+ $instance['widget-id'] = '';
+ }
+
+ $instance['type'] = 'profile';
+ ?>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>">
+ <?php esc_html_e( 'Title:', 'jetpack' ); ?>
+ </label>
+ <input
+ class="widefat"
+ id="<?php echo $this->get_field_id( 'title' ); ?>"
+ name="<?php echo $this->get_field_name( 'title' ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $instance['title'] ); ?>"
+ />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'width' ); ?>">
+ <?php esc_html_e( 'Maximum Width (px; 220 to 1200):', 'jetpack' ); ?>
+ </label>
+ <input
+ class="widefat"
+ id="<?php echo $this->get_field_id( 'width' ); ?>"
+ name="<?php echo $this->get_field_name( 'width' ); ?>"
+ type="number" min="220" max="1200"
+ value="<?php echo esc_attr( $instance['width'] ); ?>"
+ />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'height' ); ?>">
+ <?php esc_html_e( 'Height (px; at least 200):', 'jetpack' ); ?>
+ </label>
+ <input
+ class="widefat"
+ id="<?php echo $this->get_field_id( 'height' ); ?>"
+ name="<?php echo $this->get_field_name( 'height' ); ?>"
+ type="number" min="200"
+ value="<?php echo esc_attr( $instance['height'] ); ?>"
+ />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'tweet-limit' ); ?>">
+ <?php esc_html_e( '# of Tweets Shown (1 to 20):', 'jetpack' ); ?>
+ </label>
+ <input
+ class="widefat"
+ id="<?php echo $this->get_field_id( 'tweet-limit' ); ?>"
+ name="<?php echo $this->get_field_name( 'tweet-limit' ); ?>"
+ type="number" min="1" max="20"
+ value="<?php echo esc_attr( $instance['tweet-limit'] ); ?>"
+ />
+ </p>
+
+ <p class="jetpack-twitter-timeline-widget-id-container">
+ <label for="<?php echo $this->get_field_id( 'widget-id' ); ?>">
+ <?php esc_html_e( 'Twitter Username:', 'jetpack' ); ?>
+ <?php echo $this->get_docs_link( '#twitter-username' ); ?>
+ </label>
+ <input
+ class="widefat"
+ id="<?php echo $this->get_field_id( 'widget-id' ); ?>"
+ name="<?php echo $this->get_field_name( 'widget-id' ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $instance['widget-id'] ); ?>"
+ />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'chrome-noheader' ); ?>">
+ <?php esc_html_e( 'Layout Options:', 'jetpack' ); ?>
+ </label>
+ <br />
+ <input
+ type="checkbox"<?php checked( in_array( 'noheader', $instance['chrome'] ) ); ?>
+ id="<?php echo $this->get_field_id( 'chrome-noheader' ); ?>"
+ name="<?php echo $this->get_field_name( 'chrome' ); ?>[]"
+ value="noheader"
+ />
+ <label for="<?php echo $this->get_field_id( 'chrome-noheader' ); ?>">
+ <?php esc_html_e( 'No Header', 'jetpack' ); ?>
+ </label>
+ <br />
+ <input
+ type="checkbox"<?php checked( in_array( 'nofooter', $instance['chrome'] ) ); ?>
+ id="<?php echo $this->get_field_id( 'chrome-nofooter' ); ?>"
+ name="<?php echo $this->get_field_name( 'chrome' ); ?>[]"
+ value="nofooter"
+ />
+ <label for="<?php echo $this->get_field_id( 'chrome-nofooter' ); ?>">
+ <?php esc_html_e( 'No Footer', 'jetpack' ); ?>
+ </label>
+ <br />
+ <input
+ type="checkbox"<?php checked( in_array( 'noborders', $instance['chrome'] ) ); ?>
+ id="<?php echo $this->get_field_id( 'chrome-noborders' ); ?>"
+ name="<?php echo $this->get_field_name( 'chrome' ); ?>[]"
+ value="noborders"
+ />
+ <label for="<?php echo $this->get_field_id( 'chrome-noborders' ); ?>">
+ <?php esc_html_e( 'No Borders', 'jetpack' ); ?>
+ </label>
+ <br />
+ <input
+ type="checkbox"<?php checked( in_array( 'noscrollbar', $instance['chrome'] ) ); ?>
+ id="<?php echo $this->get_field_id( 'chrome-noscrollbar' ); ?>"
+ name="<?php echo $this->get_field_name( 'chrome' ); ?>[]"
+ value="noscrollbar"
+ />
+ <label for="<?php echo $this->get_field_id( 'chrome-noscrollbar' ); ?>">
+ <?php esc_html_e( 'No Scrollbar', 'jetpack' ); ?>
+ </label>
+ <br />
+ <input
+ type="checkbox"<?php checked( in_array( 'transparent', $instance['chrome'] ) ); ?>
+ id="<?php echo $this->get_field_id( 'chrome-transparent' ); ?>"
+ name="<?php echo $this->get_field_name( 'chrome' ); ?>[]"
+ value="transparent"
+ />
+ <label for="<?php echo $this->get_field_id( 'chrome-transparent' ); ?>">
+ <?php esc_html_e( 'Transparent Background', 'jetpack' ); ?>
+ </label>
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'link-color' ); ?>">
+ <?php _e( 'Link Color (hex):', 'jetpack' ); ?>
+ </label>
+ <input
+ class="widefat"
+ id="<?php echo $this->get_field_id( 'link-color' ); ?>"
+ name="<?php echo $this->get_field_name( 'link-color' ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $instance['link-color'] ); ?>"
+ />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'border-color' ); ?>">
+ <?php _e( 'Border Color (hex):', 'jetpack' ); ?>
+ </label>
+ <input
+ class="widefat"
+ id="<?php echo $this->get_field_id( 'border-color' ); ?>"
+ name="<?php echo $this->get_field_name( 'border-color' ); ?>"
+ type="text"
+ value="<?php echo esc_attr( $instance['border-color'] ); ?>"
+ />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'theme' ); ?>">
+ <?php _e( 'Timeline Theme:', 'jetpack' ); ?>
+ </label>
+ <select
+ name="<?php echo $this->get_field_name( 'theme' ); ?>"
+ id="<?php echo $this->get_field_id( 'theme' ); ?>"
+ class="widefat"
+ >
+ <option value="light"<?php selected( $instance['theme'], 'light' ); ?>>
+ <?php esc_html_e( 'Light', 'jetpack' ); ?>
+ </option>
+ <option value="dark"<?php selected( $instance['theme'], 'dark' ); ?>>
+ <?php esc_html_e( 'Dark', 'jetpack' ); ?>
+ </option>
+ </select>
+ </p>
+ <?php
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/upcoming-events.php b/plugins/jetpack/modules/widgets/upcoming-events.php
new file mode 100644
index 00000000..36a0257e
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/upcoming-events.php
@@ -0,0 +1,131 @@
+<?php
+
+class Jetpack_Upcoming_Events_Widget extends WP_Widget {
+ function __construct() {
+ parent::__construct(
+ 'upcoming_events_widget',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Upcoming Events', 'jetpack' ) ),
+ array(
+ 'description' => __( 'Display upcoming events from an iCalendar feed.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+ if ( is_active_widget( false, false, $this->id_base ) ) {
+ add_action( 'wp_head', array( $this, 'css' ) );
+ }
+ }
+
+ function css() {
+?>
+<style type="text/css">
+.upcoming-events li {
+ margin-bottom: 10px;
+}
+.upcoming-events li span {
+ display: block;
+}
+</style>
+<?php
+ }
+
+ function form( $instance ) {
+ $defaults = array(
+ 'title' => __( 'Upcoming Events', 'jetpack' ),
+ 'feed-url' => '',
+ 'count' => 3,
+ );
+ $instance = array_merge( $defaults, (array) $instance );
+?>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'feed-url' ); ?>"><?php _e( 'iCalendar Feed URL:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'feed-url' ); ?>" name="<?php echo $this->get_field_name( 'feed-url' ); ?>" type="text" value="<?php echo esc_attr( $instance['feed-url'] ); ?>" />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'count' ); ?>"><?php _e( 'Items to show:', 'jetpack' ); ?></label>
+ <select id="<?php echo $this->get_field_id( 'count' ); ?>" name="<?php echo $this->get_field_name( 'count' ); ?>">
+ <?php
+ $i = 1;
+ while ( $i <= 10 ) {
+ ?>
+ <option <?php selected( $instance['count'], $i ); ?>><?php echo $i; ?></option>
+ <?php $i++; } ?>
+ <option value="0" <?php selected( $instance['count'], 0 ); ?>><?php _e( 'All', 'jetpack' ); ?></option>
+ </select>
+ </p>
+<?php
+ }
+
+ function update( $new_instance, $old_instance ) {
+ $instance['title'] = strip_tags( $new_instance['title'] );
+ $instance['feed-url'] = strip_tags( $new_instance['feed-url'] );
+ $instance['count'] = min( absint( $new_instance['count'] ), 10 ); // 10 or less
+ return $instance;
+ }
+
+ function widget( $args, $instance ) {
+ jetpack_require_lib( 'icalendar-reader' );
+
+ $ical = new iCalendarReader();
+ $events = $ical->get_events( $instance['feed-url'], $instance['count'] );
+ $events = $this->apply_timezone_offset( $events );
+ $ical->timezone = null;
+
+ echo $args['before_widget'];
+ if ( ! empty( $instance['title'] ) ) {
+ echo $args['before_title'];
+ echo esc_html( $instance['title'] );
+ echo $args['after_title'];
+ }
+
+ if ( ! $events ) : // nothing to display?
+?>
+ <p><?php echo __( 'No upcoming events', 'jetpack' ); ?></p>
+<?php
+ else :
+?>
+ <ul class="upcoming-events">
+ <?php foreach ( $events as $event ) : ?>
+ <li>
+ <strong class="event-summary"><?php echo $ical->escape( stripslashes( $event['SUMMARY'] ) ); ?></strong>
+ <span class="event-when"><?php echo $ical->formatted_date( $event ); ?></span>
+ <?php if ( ! empty( $event['LOCATION'] ) ) : ?>
+ <span class="event-location"><?php echo $ical->escape( stripslashes( $event['LOCATION'] ) ); ?></span>
+ <?php endif; ?>
+ <?php if ( ! empty( $event['DESCRIPTION'] ) ) : ?>
+ <span class="event-description"><?php echo wp_trim_words( $ical->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></span>
+ <?php endif; ?>
+ </li>
+ <?php endforeach; ?>
+ </ul>
+<?php
+ endif;
+
+ echo $args['after_widget'];
+
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'grofile' );
+ }
+
+ // Left this function here for backward compatibility
+ // just incase a site using jetpack is also using this function
+ function apply_timezone_offset( $events ) {
+ jetpack_require_lib( 'icalendar-reader' );
+
+ $ical = new iCalendarReader();
+ return $ical->apply_timezone_offset( $events );
+ }
+}
+
+function upcoming_events_register_widgets() {
+ register_widget( 'Jetpack_Upcoming_Events_Widget' );
+}
+
+add_action( 'widgets_init', 'upcoming_events_register_widgets' );
diff --git a/plugins/jetpack/modules/widgets/wordpress-post-widget.php b/plugins/jetpack/modules/widgets/wordpress-post-widget.php
new file mode 100644
index 00000000..f518ad61
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/wordpress-post-widget.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Plugin Name: Display Recent WordPress Posts Widget
+ * Description: Displays recent posts from a WordPress.com or Jetpack-enabled self-hosted WordPress site.
+ * Version: 1.0
+ * Author: Brad Angelcyk, Kathryn Presner, Justin Shreve, Carolyn Sonnek
+ * Author URI: http://automattic.com
+ * License: GPL2
+ */
+
+/**
+ * Disable direct access/execution to/of the widget code.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+require dirname( __FILE__ ) . '/wordpress-post-widget/class.jetpack-display-posts-widget-base.php';
+require dirname( __FILE__ ) . '/wordpress-post-widget/class.jetpack-display-posts-widget.php';
+
+add_action( 'widgets_init', 'jetpack_display_posts_widget' );
+function jetpack_display_posts_widget() {
+ register_widget( 'Jetpack_Display_Posts_Widget' );
+}
+
+
+/**
+ * Cron tasks
+ */
+
+add_filter( 'cron_schedules', 'jetpack_display_posts_widget_cron_intervals' );
+
+/**
+ * Adds 10 minute running interval to the cron schedules.
+ *
+ * @param array $current_schedules Currently defined schedules list.
+ *
+ * @return array
+ */
+function jetpack_display_posts_widget_cron_intervals( $current_schedules ) {
+
+ /**
+ * Only add the 10 minute interval if it wasn't already set.
+ */
+ if ( ! isset( $current_schedules['minutes_10'] ) ) {
+ $current_schedules['minutes_10'] = array(
+ 'interval' => 10 * MINUTE_IN_SECONDS,
+ 'display' => 'Every 10 minutes',
+ );
+ }
+
+ return $current_schedules;
+}
+
+/**
+ * Execute the cron task
+ */
+add_action( 'jetpack_display_posts_widget_cron_update', 'jetpack_display_posts_update_cron_action' );
+function jetpack_display_posts_update_cron_action() {
+ $widget = new Jetpack_Display_Posts_Widget();
+ $widget->cron_task();
+}
+
+/**
+ * Handle activation procedures for the cron.
+ *
+ * `updating_jetpack_version` - Handle cron activation when Jetpack gets updated. It's here
+ * to cover the first cron activation after the update.
+ *
+ * `jetpack_activate_module_widgets` - Activate the cron when the Extra Sidebar widgets are activated.
+ *
+ * `activated_plugin` - Activate the cron when Jetpack gets activated.
+ *
+ */
+add_action( 'updating_jetpack_version', 'jetpack_display_posts_widget_conditionally_activate_cron' );
+add_action( 'jetpack_activate_module_widgets', 'Jetpack_Display_Posts_Widget::activate_cron' );
+add_action( 'activated_plugin', 'jetpack_conditionally_activate_cron_on_plugin_activation' );
+
+/**
+ * Executed when Jetpack gets activated. Tries to activate the cron if it is needed.
+ *
+ * @param string $plugin_file_name The plugin file that was activated.
+ */
+function jetpack_conditionally_activate_cron_on_plugin_activation( $plugin_file_name ) {
+ if ( plugin_basename( JETPACK__PLUGIN_FILE ) === $plugin_file_name ) {
+ jetpack_display_posts_widget_conditionally_activate_cron();
+ }
+}
+
+/**
+ * Activates the cron only when needed.
+ * @see Jetpack_Display_Posts_Widget::should_cron_be_running
+ */
+function jetpack_display_posts_widget_conditionally_activate_cron() {
+ $widget = new Jetpack_Display_Posts_Widget();
+ if ( $widget->should_cron_be_running() ) {
+ $widget->activate_cron();
+ }
+
+ unset( $widget );
+}
+
+/**
+ * End of cron activation handling.
+ */
+
+
+/**
+ * Handle deactivation procedures where they are needed.
+ *
+ * If Extra Sidebar Widgets module is deactivated, the cron is not needed.
+ *
+ * If Jetpack is deactivated, the cron is not needed.
+ */
+add_action( 'jetpack_deactivate_module_widgets', 'Jetpack_Display_Posts_Widget::deactivate_cron_static' );
+register_deactivation_hook( plugin_basename( JETPACK__PLUGIN_FILE ), 'Jetpack_Display_Posts_Widget::deactivate_cron_static' );
diff --git a/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget-base.php b/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget-base.php
new file mode 100644
index 00000000..8a59545a
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget-base.php
@@ -0,0 +1,843 @@
+<?php
+
+/*
+ * For back-compat, the final widget class must be named
+ * Jetpack_Display_Posts_Widget.
+ *
+ * For convenience, it's nice to have a widget class constructor with no
+ * arguments. Otherwise, we have to register the widget with an instance
+ * instead of a class name. This makes unregistering annoying.
+ *
+ * Both WordPress.com and Jetpack implement the final widget class by
+ * extending this __Base class and adding data fetching and storage.
+ *
+ * This would be a bit cleaner with dependency injection, but we already
+ * use mocking to test, so it's not a big win.
+ *
+ * That this widget is currently implemented as these two classes
+ * is an implementation detail and should not be depended on :)
+ */
+abstract class Jetpack_Display_Posts_Widget__Base extends WP_Widget {
+ /**
+ * @var string Remote service API URL prefix.
+ */
+ public $service_url = 'https://public-api.wordpress.com/rest/v1.1/';
+
+ public function __construct() {
+ parent::__construct(
+ // internal id
+ 'jetpack_display_posts_widget',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', __( 'Display WordPress Posts', 'jetpack' ) ),
+ array(
+ 'description' => __( 'Displays a list of recent posts from another WordPress.com or Jetpack-enabled blog.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+
+ if ( is_customize_preview() ) {
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+ }
+ }
+
+ /**
+ * Enqueue CSS and JavaScript.
+ *
+ * @since 4.0.0
+ */
+ public function enqueue_scripts() {
+ wp_enqueue_style( 'jetpack_display_posts_widget', plugins_url( 'style.css', __FILE__ ) );
+ }
+
+
+ // DATA STORE: Must implement
+
+ /**
+ * Gets blog data from the cache.
+ *
+ * @param string $site
+ *
+ * @return array|WP_Error
+ */
+ abstract public function get_blog_data( $site );
+
+ /**
+ * Update a widget instance.
+ *
+ * @param string $site The site to fetch the latest data for.
+ *
+ * @return array - the new data
+ */
+ abstract public function update_instance( $site );
+
+
+ // WIDGET API
+
+ /**
+ * Set up the widget display on the front end.
+ *
+ * @param array $args
+ * @param array $instance
+ */
+ public function widget( $args, $instance ) {
+ /** This action is documented in modules/widgets/gravatar-profile.php */
+ do_action( 'jetpack_stats_extra', 'widget_view', 'display_posts' );
+
+ // Enqueue front end assets.
+ $this->enqueue_scripts();
+
+ $content = $args['before_widget'];
+
+ if ( empty( $instance['url'] ) ) {
+ if ( current_user_can( 'manage_options' ) ) {
+ $content .= '<p>';
+ /* Translators: the "Blog URL" field mentioned is the input field labeled as such in the widget form. */
+ $content .= esc_html__( 'The Blog URL is not properly setup in the widget.', 'jetpack' );
+ $content .= '</p>';
+ }
+ $content .= $args['after_widget'];
+
+ echo $content;
+ return;
+ }
+
+ $data = $this->get_blog_data( $instance['url'] );
+ // check for errors
+ if ( is_wp_error( $data ) || empty( $data['site_info']['data'] ) ) {
+ $content .= '<p>' . __( 'Cannot load blog information at this time.', 'jetpack' ) . '</p>';
+ $content .= $args['after_widget'];
+
+ echo $content;
+ return;
+ }
+
+ $site_info = $data['site_info']['data'];
+
+ if ( ! empty( $instance['title'] ) ) {
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
+ $instance['title'] = apply_filters( 'widget_title', $instance['title'] );
+ $content .= $args['before_title'] . esc_html( $instance['title'] . ': ' . $site_info->name ) . $args['after_title'];
+ }
+ else {
+ $content .= $args['before_title'] . esc_html( $site_info->name ) . $args['after_title'];
+ }
+
+ $content .= '<div class="jetpack-display-remote-posts">';
+
+ if ( is_wp_error( $data['posts']['data'] ) || empty( $data['posts']['data'] ) ) {
+ $content .= '<p>' . __( 'Cannot load blog posts at this time.', 'jetpack' ) . '</p>';
+ $content .= '</div><!-- .jetpack-display-remote-posts -->';
+ $content .= $args['after_widget'];
+
+ echo $content;
+ return;
+ }
+
+ $posts_list = $data['posts']['data'];
+
+ /**
+ * Show only as much posts as we need. If we have less than configured amount,
+ * we must show only that much posts.
+ */
+ $number_of_posts = min( $instance['number_of_posts'], count( $posts_list ) );
+
+ for ( $i = 0; $i < $number_of_posts; $i ++ ) {
+ $single_post = $posts_list[ $i ];
+ $post_title = ( $single_post['title'] ) ? $single_post['title'] : '( No Title )';
+
+ $target = '';
+ if ( isset( $instance['open_in_new_window'] ) && $instance['open_in_new_window'] == true ) {
+ $target = ' target="_blank" rel="noopener"';
+ }
+ $content .= '<h4><a href="' . esc_url( $single_post['url'] ) . '"' . $target . '>' . esc_html( $post_title ) . '</a></h4>' . "\n";
+ if ( ( $instance['featured_image'] == true ) && ( ! empty ( $single_post['featured_image'] ) ) ) {
+ $featured_image = $single_post['featured_image'];
+ /**
+ * Allows setting up custom Photon parameters to manipulate the image output in the Display Posts widget.
+ *
+ * @see https://developer.wordpress.com/docs/photon/
+ *
+ * @module widgets
+ *
+ * @since 3.6.0
+ *
+ * @param array $args Array of Photon Parameters.
+ */
+ $image_params = apply_filters( 'jetpack_display_posts_widget_image_params', array() );
+ $content .= '<a title="' . esc_attr( $post_title ) . '" href="' . esc_url( $single_post['url'] ) . '"' . $target . '><img src="' . jetpack_photon_url( $featured_image, $image_params ) . '" alt="' . esc_attr( $post_title ) . '"/></a>';
+ }
+
+ if ( $instance['show_excerpts'] == true ) {
+ $content .= $single_post['excerpt'];
+ }
+ }
+
+ $content .= '</div><!-- .jetpack-display-remote-posts -->';
+ $content .= $args['after_widget'];
+
+ /**
+ * Filter the WordPress Posts widget content.
+ *
+ * @module widgets
+ *
+ * @since 4.7.0
+ *
+ * @param string $content Widget content.
+ */
+ echo apply_filters( 'jetpack_display_posts_widget_content', $content );
+ }
+
+ /**
+ * Display the widget administration form.
+ *
+ * @param array $instance Widget instance configuration.
+ *
+ * @return string|void
+ */
+ public function form( $instance ) {
+
+ /**
+ * Initialize widget configuration variables.
+ */
+ $title = ( isset( $instance['title'] ) ) ? $instance['title'] : __( 'Recent Posts', 'jetpack' );
+ $url = ( isset( $instance['url'] ) ) ? $instance['url'] : '';
+ $number_of_posts = ( isset( $instance['number_of_posts'] ) ) ? $instance['number_of_posts'] : 5;
+ $open_in_new_window = ( isset( $instance['open_in_new_window'] ) ) ? $instance['open_in_new_window'] : false;
+ $featured_image = ( isset( $instance['featured_image'] ) ) ? $instance['featured_image'] : false;
+ $show_excerpts = ( isset( $instance['show_excerpts'] ) ) ? $instance['show_excerpts'] : false;
+
+
+ /**
+ * Check if the widget instance has errors available.
+ *
+ * Only do so if a URL is set.
+ */
+ $update_errors = array();
+
+ if ( ! empty( $url ) ) {
+ $data = $this->get_blog_data( $url );
+ $update_errors = $this->extract_errors_from_blog_data( $data );
+ }
+
+ ?>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
+ </p>
+
+ <p>
+ <label for="<?php echo $this->get_field_id( 'url' ); ?>"><?php _e( 'Blog URL:', 'jetpack' ); ?></label>
+ <input class="widefat" id="<?php echo $this->get_field_id( 'url' ); ?>" name="<?php echo $this->get_field_name( 'url' ); ?>" type="text" value="<?php echo esc_attr( $url ); ?>" />
+ <i>
+ <?php _e( "Enter a WordPress.com or Jetpack WordPress site URL.", 'jetpack' ); ?>
+ </i>
+ <?php
+ /**
+ * Show an error if the URL field was left empty.
+ *
+ * The error is shown only when the widget was already saved.
+ */
+ if ( empty( $url ) && ! preg_match( '/__i__|%i%/', $this->id ) ) {
+ ?>
+ <br />
+ <i class="error-message"><?php echo __( 'You must specify a valid blog URL!', 'jetpack' ); ?></i>
+ <?php
+ }
+ ?>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'number_of_posts' ); ?>"><?php _e( 'Number of Posts to Display:', 'jetpack' ); ?></label>
+ <select name="<?php echo $this->get_field_name( 'number_of_posts' ); ?>">
+ <?php
+ for ( $i = 1; $i <= 10; $i ++ ) {
+ echo '<option value="' . $i . '" ' . selected( $number_of_posts, $i ) . '>' . $i . '</option>';
+ }
+ ?>
+ </select>
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'open_in_new_window' ); ?>"><?php _e( 'Open links in new window/tab:', 'jetpack' ); ?></label>
+ <input type="checkbox" name="<?php echo $this->get_field_name( 'open_in_new_window' ); ?>" <?php checked( $open_in_new_window, 1 ); ?> />
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'featured_image' ); ?>"><?php _e( 'Show Featured Image:', 'jetpack' ); ?></label>
+ <input type="checkbox" name="<?php echo $this->get_field_name( 'featured_image' ); ?>" <?php checked( $featured_image, 1 ); ?> />
+ </p>
+ <p>
+ <label for="<?php echo $this->get_field_id( 'show_excerpts' ); ?>"><?php _e( 'Show Excerpts:', 'jetpack' ); ?></label>
+ <input type="checkbox" name="<?php echo $this->get_field_name( 'show_excerpts' ); ?>" <?php checked( $show_excerpts, 1 ); ?> />
+ </p>
+
+ <?php
+
+ /**
+ * Show error messages.
+ */
+ if ( ! empty( $update_errors['message'] ) ) {
+
+ /**
+ * Prepare the error messages.
+ */
+
+ $where_message = '';
+ switch ( $update_errors['where'] ) {
+ case 'posts':
+ $where_message .= __( 'An error occurred while downloading blog posts list', 'jetpack' );
+ break;
+
+ /**
+ * If something else, beside `posts` and `site_info` broke,
+ * don't handle it and default to blog `information`,
+ * as it is generic enough.
+ */
+ case 'site_info':
+ default:
+ $where_message .= __( 'An error occurred while downloading blog information', 'jetpack' );
+ break;
+ }
+
+ ?>
+ <p class="error-message">
+ <?php echo esc_html( $where_message ); ?>:
+ <br />
+ <i>
+ <?php echo esc_html( $update_errors['message'] ); ?>
+ <?php
+ /**
+ * If there is any debug - show it here.
+ */
+ if ( ! empty( $update_errors['debug'] ) ) {
+ ?>
+ <br />
+ <br />
+ <?php esc_html_e( 'Detailed information', 'jetpack' ); ?>:
+ <br />
+ <?php echo esc_html( $update_errors['debug'] ); ?>
+ <?php
+ }
+ ?>
+ </i>
+ </p>
+
+ <?php
+ }
+ }
+
+ public function update( $new_instance, $old_instance ) {
+
+ $instance = array();
+ $instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
+ $instance['url'] = ( ! empty( $new_instance['url'] ) ) ? strip_tags( trim( $new_instance['url'] ) ) : '';
+ $instance['url'] = preg_replace( "!^https?://!is", "", $instance['url'] );
+ $instance['url'] = untrailingslashit( $instance['url'] );
+
+
+ /**
+ * Check if the URL should be with or without the www prefix before saving.
+ */
+ if ( ! empty( $instance['url'] ) ) {
+ $blog_data = $this->fetch_blog_data( $instance['url'], array(), true );
+
+ if ( is_wp_error( $blog_data['site_info']['error'] ) && 'www.' === substr( $instance['url'], 0, 4 ) ) {
+ $blog_data = $this->fetch_blog_data( substr( $instance['url'], 4 ), array(), true );
+
+ if ( ! is_wp_error( $blog_data['site_info']['error'] ) ) {
+ $instance['url'] = substr( $instance['url'], 4 );
+ }
+ }
+ }
+
+ $instance['number_of_posts'] = ( ! empty( $new_instance['number_of_posts'] ) ) ? intval( $new_instance['number_of_posts'] ) : '';
+ $instance['open_in_new_window'] = ( ! empty( $new_instance['open_in_new_window'] ) ) ? true : '';
+ $instance['featured_image'] = ( ! empty( $new_instance['featured_image'] ) ) ? true : '';
+ $instance['show_excerpts'] = ( ! empty( $new_instance['show_excerpts'] ) ) ? true : '';
+
+ /**
+ * If there is no cache entry for the specified URL, run a forced update.
+ *
+ * @see get_blog_data Returns WP_Error if the cache is empty, which is what is needed here.
+ */
+ $cached_data = $this->get_blog_data( $instance['url'] );
+
+ if ( is_wp_error( $cached_data ) ) {
+ $this->update_instance( $instance['url'] );
+ }
+
+ return $instance;
+ }
+
+
+ // DATA PROCESSING
+
+ /**
+ * Expiring transients have a name length maximum of 45 characters,
+ * so this function returns an abbreviated MD5 hash to use instead of
+ * the full URI.
+ *
+ * @param string $site Site to get the hash for.
+ *
+ * @return string
+ */
+ public function get_site_hash( $site ) {
+ return substr( md5( $site ), 0, 21 );
+ }
+
+ /**
+ * Fetch a remote service endpoint and parse it.
+ *
+ * Timeout is set to 15 seconds right now, because sometimes the WordPress API
+ * takes more than 5 seconds to fully respond.
+ *
+ * Caching is used here so we can avoid re-downloading the same endpoint
+ * in a single request.
+ *
+ * @param string $endpoint Parametrized endpoint to call.
+ *
+ * @param int $timeout How much time to wait for the API to respond before failing.
+ *
+ * @return array|WP_Error
+ */
+ public function fetch_service_endpoint( $endpoint, $timeout = 15 ) {
+
+ /**
+ * Holds endpoint request cache.
+ */
+ static $cache = array();
+
+ if ( ! isset( $cache[ $endpoint ] ) ) {
+ $raw_data = $this->wp_wp_remote_get( $this->service_url . ltrim( $endpoint, '/' ), array( 'timeout' => $timeout ) );
+ $cache[ $endpoint ] = $this->parse_service_response( $raw_data );
+ }
+
+ return $cache[ $endpoint ];
+ }
+
+ /**
+ * Parse data from service response.
+ * Do basic error handling for general service and data errors
+ *
+ * @param array $service_response Response from the service.
+ *
+ * @return array|WP_Error
+ */
+ public function parse_service_response( $service_response ) {
+ /**
+ * If there is an error, we add the error message to the parsed response
+ */
+ if ( is_wp_error( $service_response ) ) {
+ return new WP_Error(
+ 'general_error',
+ __( 'An error occurred fetching the remote data.', 'jetpack' ),
+ $service_response->get_error_messages()
+ );
+ }
+
+ /**
+ * Validate HTTP response code.
+ */
+ if ( 200 !== wp_remote_retrieve_response_code( $service_response ) ) {
+ return new WP_Error(
+ 'http_error',
+ __( 'An error occurred fetching the remote data.', 'jetpack' ),
+ wp_remote_retrieve_response_message( $service_response )
+ );
+ }
+
+
+ /**
+ * Extract service response body from the request.
+ */
+
+ $service_response_body = wp_remote_retrieve_body( $service_response );
+
+
+ /**
+ * No body has been set in the response. This should be pretty bad.
+ */
+ if ( ! $service_response_body ) {
+ return new WP_Error(
+ 'no_body',
+ __( 'Invalid remote response.', 'jetpack' ),
+ 'No body in response.'
+ );
+ }
+
+ /**
+ * Parse the JSON response from the API. Convert to associative array.
+ */
+ $parsed_data = json_decode( $service_response_body );
+
+ /**
+ * If there is a problem with parsing the posts return an empty array.
+ */
+ if ( is_null( $parsed_data ) ) {
+ return new WP_Error(
+ 'no_body',
+ __( 'Invalid remote response.', 'jetpack' ),
+ 'Invalid JSON from remote.'
+ );
+ }
+
+ /**
+ * Check for errors in the parsed body.
+ */
+ if ( isset( $parsed_data->error ) ) {
+ return new WP_Error(
+ 'remote_error',
+ __( 'It looks like the WordPress site URL is incorrectly configured. Please check it in your widget settings.', 'jetpack' ),
+ $parsed_data->error
+ );
+ }
+
+ /**
+ * No errors found, return parsed data.
+ */
+ return $parsed_data;
+ }
+
+ /**
+ * Fetch site information from the WordPress public API
+ *
+ * @param string $site URL of the site to fetch the information for.
+ *
+ * @return array|WP_Error
+ */
+ public function fetch_site_info( $site ) {
+
+ $response = $this->fetch_service_endpoint( sprintf( '/sites/%s', urlencode( $site ) ) );
+
+ return $response;
+ }
+
+ /**
+ * Parse external API response from the site info call and handle errors if they occur.
+ *
+ * @param array|WP_Error $service_response The raw response to be parsed.
+ *
+ * @return array|WP_Error
+ */
+ public function parse_site_info_response( $service_response ) {
+
+ /**
+ * If the service returned an error, we pass it on.
+ */
+ if ( is_wp_error( $service_response ) ) {
+ return $service_response;
+ }
+
+ /**
+ * Check if the service returned proper site information.
+ */
+ if ( ! isset( $service_response->ID ) ) {
+ return new WP_Error(
+ 'no_site_info',
+ __( 'Invalid site information returned from remote.', 'jetpack' ),
+ 'No site ID present in the response.'
+ );
+ }
+
+ return $service_response;
+ }
+
+ /**
+ * Fetch list of posts from the WordPress public API.
+ *
+ * @param int $site_id The site to fetch the posts for.
+ *
+ * @return array|WP_Error
+ */
+ public function fetch_posts_for_site( $site_id ) {
+
+ $response = $this->fetch_service_endpoint(
+ sprintf(
+ '/sites/%1$d/posts/%2$s',
+ $site_id,
+ /**
+ * Filters the parameters used to fetch for posts in the Display Posts Widget.
+ *
+ * @see https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/
+ *
+ * @module widgets
+ *
+ * @since 3.6.0
+ *
+ * @param string $args Extra parameters to filter posts returned from the WordPress.com REST API.
+ */
+ apply_filters( 'jetpack_display_posts_widget_posts_params', '' )
+ )
+ );
+
+ return $response;
+ }
+
+ /**
+ * Parse external API response from the posts list request and handle errors if any occur.
+ *
+ * @param object|WP_Error $service_response The raw response to be parsed.
+ *
+ * @return array|WP_Error
+ */
+ public function parse_posts_response( $service_response ) {
+
+ /**
+ * If the service returned an error, we pass it on.
+ */
+ if ( is_wp_error( $service_response ) ) {
+ return $service_response;
+ }
+
+ /**
+ * Check if the service returned proper posts array.
+ */
+ if ( ! isset( $service_response->posts ) || ! is_array( $service_response->posts ) ) {
+ return new WP_Error(
+ 'no_posts',
+ __( 'No posts data returned by remote.', 'jetpack' ),
+ 'No posts information set in the returned data.'
+ );
+ }
+
+ /**
+ * Format the posts to preserve storage space.
+ */
+
+ return $this->format_posts_for_storage( $service_response );
+ }
+
+ /**
+ * Format the posts for better storage. Drop all the data that is not used.
+ *
+ * @param object $parsed_data Array of posts returned by the APIs.
+ *
+ * @return array Formatted posts or an empty array if no posts were found.
+ */
+ public function format_posts_for_storage( $parsed_data ) {
+
+ $formatted_posts = array();
+
+ /**
+ * Only go through the posts list if we have valid posts array.
+ */
+ if ( isset( $parsed_data->posts ) && is_array( $parsed_data->posts ) ) {
+
+ /**
+ * Loop through all the posts and format them appropriately.
+ */
+ foreach ( $parsed_data->posts as $single_post ) {
+
+ $prepared_post = array(
+ 'title' => $single_post->title ? $single_post->title : '',
+ 'excerpt' => $single_post->excerpt ? $single_post->excerpt : '',
+ 'featured_image' => $single_post->featured_image ? $single_post->featured_image : '',
+ 'url' => $single_post->URL,
+ );
+
+ /**
+ * Append the formatted post to the results.
+ */
+ $formatted_posts[] = $prepared_post;
+ }
+ }
+
+ return $formatted_posts;
+ }
+
+ /**
+ * Fetch site information and posts list for a site.
+ *
+ * @param string $site Site to fetch the data for.
+ * @param array $original_data Optional original data to updated.
+ *
+ * @param bool $site_data_only Fetch only site information, skip posts list.
+ *
+ * @return array Updated or new data.
+ */
+ public function fetch_blog_data( $site, $original_data = array(), $site_data_only = false ) {
+
+ /**
+ * If no optional data is supplied, initialize a new structure
+ */
+ if ( ! empty( $original_data ) ) {
+ $widget_data = $original_data;
+ }
+ else {
+ $widget_data = array(
+ 'site_info' => array(
+ 'last_check' => null,
+ 'last_update' => null,
+ 'error' => null,
+ 'data' => array(),
+ ),
+ 'posts' => array(
+ 'last_check' => null,
+ 'last_update' => null,
+ 'error' => null,
+ 'data' => array(),
+ )
+ );
+ }
+
+ /**
+ * Update check time and fetch site information.
+ */
+ $widget_data['site_info']['last_check'] = time();
+
+ $site_info_raw_data = $this->fetch_site_info( $site );
+ $site_info_parsed_data = $this->parse_site_info_response( $site_info_raw_data );
+
+
+ /**
+ * If there is an error with the fetched site info, save the error and update the checked time.
+ */
+ if ( is_wp_error( $site_info_parsed_data ) ) {
+ $widget_data['site_info']['error'] = $site_info_parsed_data;
+
+ return $widget_data;
+ }
+ /**
+ * If data is fetched successfully, update the data and set the proper time.
+ *
+ * Data is only updated if we have valid results. This is done this way so we can show
+ * something if external service is down.
+ *
+ */
+ else {
+ $widget_data['site_info']['last_update'] = time();
+ $widget_data['site_info']['data'] = $site_info_parsed_data;
+ $widget_data['site_info']['error'] = null;
+ }
+
+
+ /**
+ * If only site data is needed, return it here, don't fetch posts data.
+ */
+ if ( true === $site_data_only ) {
+ return $widget_data;
+ }
+
+ /**
+ * Update check time and fetch posts list.
+ */
+ $widget_data['posts']['last_check'] = time();
+
+ $site_posts_raw_data = $this->fetch_posts_for_site( $site_info_parsed_data->ID );
+ $site_posts_parsed_data = $this->parse_posts_response( $site_posts_raw_data );
+
+
+ /**
+ * If there is an error with the fetched posts, save the error and update the checked time.
+ */
+ if ( is_wp_error( $site_posts_parsed_data ) ) {
+ $widget_data['posts']['error'] = $site_posts_parsed_data;
+
+ return $widget_data;
+ }
+ /**
+ * If data is fetched successfully, update the data and set the proper time.
+ *
+ * Data is only updated if we have valid results. This is done this way so we can show
+ * something if external service is down.
+ *
+ */
+ else {
+ $widget_data['posts']['last_update'] = time();
+ $widget_data['posts']['data'] = $site_posts_parsed_data;
+ $widget_data['posts']['error'] = null;
+ }
+
+ return $widget_data;
+ }
+
+ /**
+ * Scan and extract first error from blog data array.
+ *
+ * @param array|WP_Error $blog_data Blog data to scan for errors.
+ *
+ * @return string First error message found
+ */
+ public function extract_errors_from_blog_data( $blog_data ) {
+
+ $errors = array(
+ 'message' => '',
+ 'debug' => '',
+ 'where' => '',
+ );
+
+
+ /**
+ * When the cache result is an error. Usually when the cache is empty.
+ * This is not an error case for now.
+ */
+ if ( is_wp_error( $blog_data ) ) {
+ return $errors;
+ }
+
+ /**
+ * Loop through `site_info` and `posts` keys of $blog_data.
+ */
+ foreach ( array( 'site_info', 'posts' ) as $info_key ) {
+
+ /**
+ * Contains information on which stage the error ocurred.
+ */
+ $errors['where'] = $info_key;
+
+ /**
+ * If an error is set, we want to check it for usable messages.
+ */
+ if ( isset( $blog_data[ $info_key ]['error'] ) && ! empty( $blog_data[ $info_key ]['error'] ) ) {
+
+ /**
+ * Extract error message from the error, if possible.
+ */
+ if ( is_wp_error( $blog_data[ $info_key ]['error'] ) ) {
+ /**
+ * In the case of WP_Error we want to have the error message
+ * and the debug information available.
+ */
+ $error_messages = $blog_data[ $info_key ]['error']->get_error_messages();
+ $errors['message'] = reset( $error_messages );
+
+ $extra_data = $blog_data[ $info_key ]['error']->get_error_data();
+ if ( is_array( $extra_data ) ) {
+ $errors['debug'] = implode( '; ', $extra_data );
+ }
+ else {
+ $errors['debug'] = $extra_data;
+ }
+
+ break;
+ }
+ elseif ( is_array( $blog_data[ $info_key ]['error'] ) ) {
+ /**
+ * In this case we don't have debug information, because
+ * we have no way to know the format. The widget works with
+ * WP_Error objects only.
+ */
+ $errors['message'] = reset( $blog_data[ $info_key ]['error'] );
+ break;
+ }
+
+ /**
+ * We do nothing if no usable error is found.
+ */
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * This is just to make method mocks in the unit tests easier.
+ *
+ * @param string $url The URL to fetch
+ * @param array $args Optional. Request arguments.
+ *
+ * @return array|WP_Error
+ *
+ * @codeCoverageIgnore
+ */
+ public function wp_wp_remote_get( $url, $args = array() ) {
+ return wp_remote_get( $url, $args );
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget.php b/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget.php
new file mode 100644
index 00000000..265e2ebb
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget.php
@@ -0,0 +1,274 @@
+<?php
+
+/*
+ * Display a list of recent posts from a WordPress.com or Jetpack-enabled blog.
+ */
+
+class Jetpack_Display_Posts_Widget extends Jetpack_Display_Posts_Widget__Base {
+ /**
+ * @var string Widget options key prefix.
+ */
+ public $widget_options_key_prefix = 'display_posts_site_data_';
+
+ /**
+ * @var string The name of the cron that will update widget data.
+ */
+ public static $cron_name = 'jetpack_display_posts_widget_cron_update';
+
+
+ // DATA STORE
+
+ /**
+ * Gets blog data from the cache.
+ *
+ * @param string $site
+ *
+ * @return array|WP_Error
+ */
+ public function get_blog_data( $site ) {
+ // load from cache, if nothing return an error
+ $site_hash = $this->get_site_hash( $site );
+
+ $cached_data = $this->wp_get_option( $this->widget_options_key_prefix . $site_hash );
+
+ /**
+ * If the cache is empty, return an empty_cache error.
+ */
+ if ( false === $cached_data ) {
+ return new WP_Error(
+ 'empty_cache',
+ __( 'Information about this blog is currently being retrieved.', 'jetpack' )
+ );
+ }
+
+ return $cached_data;
+
+ }
+
+ /**
+ * Update a widget instance.
+ *
+ * @param string $site The site to fetch the latest data for.
+ *
+ * @return array - the new data
+ */
+ public function update_instance( $site ) {
+
+ /**
+ * Fetch current information for a site.
+ */
+ $site_hash = $this->get_site_hash( $site );
+
+ $option_key = $this->widget_options_key_prefix . $site_hash;
+
+ $instance_data = $this->wp_get_option( $option_key );
+
+ /**
+ * Fetch blog data and save it in $instance_data.
+ */
+ $new_data = $this->fetch_blog_data( $site, $instance_data );
+
+ /**
+ * If the option doesn't exist yet - create a new option
+ */
+ if ( false === $instance_data ) {
+ $this->wp_add_option( $option_key, $new_data );
+ }
+ else {
+ $this->wp_update_option( $option_key, $new_data );
+ }
+
+ return $new_data;
+ }
+
+
+ // WIDGET API
+
+ public function update( $new_instance, $old_instance ) {
+ $instance = parent::update( $new_instance, $old_instance );
+
+ /**
+ * Forcefully activate the update cron when saving widget instance.
+ *
+ * So we can be sure that it will be running later.
+ */
+ $this->activate_cron();
+
+ return $instance;
+ }
+
+
+ // CRON
+
+ /**
+ * Activates widget update cron task.
+ */
+ public static function activate_cron() {
+ if ( ! wp_next_scheduled( self::$cron_name ) ) {
+ wp_schedule_event( time(), 'minutes_10', self::$cron_name );
+ }
+ }
+
+ /**
+ * Deactivates widget update cron task.
+ *
+ * This is a wrapper over the static method as it provides some syntactic sugar.
+ */
+ public function deactivate_cron() {
+ self::deactivate_cron_static();
+ }
+
+ /**
+ * Deactivates widget update cron task.
+ */
+ public static function deactivate_cron_static() {
+ $next_scheduled_time = wp_next_scheduled( self::$cron_name );
+ wp_unschedule_event( $next_scheduled_time, self::$cron_name );
+ }
+
+ /**
+ * Checks if the update cron should be running and returns appropriate result.
+ *
+ * @return bool If the cron should be running or not.
+ */
+ public function should_cron_be_running() {
+ /**
+ * The cron doesn't need to run empty loops.
+ */
+ $widget_instances = $this->get_instances_sites();
+
+ if ( empty( $widget_instances ) || ! is_array( $widget_instances ) ) {
+ return false;
+ }
+
+ if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
+ /**
+ * If Jetpack is not active or in development mode, we don't want to update widget data.
+ */
+ if ( ! Jetpack::is_active() && ! Jetpack::is_development_mode() ) {
+ return false;
+ }
+
+ /**
+ * If Extra Sidebar Widgets module is not active, we don't need to update widget data.
+ */
+ if ( ! Jetpack::is_module_active( 'widgets' ) ) {
+ return false;
+ }
+ }
+
+ /**
+ * If none of the above checks failed, then we definitely want to update widget data.
+ */
+ return true;
+ }
+
+ /**
+ * Main cron code. Updates all instances of the widget.
+ *
+ * @return bool
+ */
+ public function cron_task() {
+
+ /**
+ * If the cron should not be running, disable it.
+ */
+ if ( false === $this->should_cron_be_running() ) {
+ return true;
+ }
+
+ $instances_to_update = $this->get_instances_sites();
+
+ /**
+ * If no instances are found to be updated - stop.
+ */
+ if ( empty( $instances_to_update ) || ! is_array( $instances_to_update ) ) {
+ return true;
+ }
+
+ foreach ( $instances_to_update as $site_url ) {
+ $this->update_instance( $site_url );
+ }
+
+ return true;
+ }
+
+ /**
+ * Get a list of unique sites from all instances of the widget.
+ *
+ * @return array|bool
+ */
+ public function get_instances_sites() {
+
+ $widget_settings = $this->wp_get_option( 'widget_jetpack_display_posts_widget' );
+
+ /**
+ * If the widget still hasn't been added anywhere, the config will not be present.
+ *
+ * In such case we don't want to continue execution.
+ */
+ if ( false === $widget_settings || ! is_array( $widget_settings ) ) {
+ return false;
+ }
+
+ $urls = array();
+
+ foreach ( $widget_settings as $widget_instance_data ) {
+ if ( isset( $widget_instance_data['url'] ) && ! empty( $widget_instance_data['url'] ) ) {
+ $urls[] = $widget_instance_data['url'];
+ }
+ }
+
+ /**
+ * Make sure only unique URLs are returned.
+ */
+ $urls = array_unique( $urls );
+
+ return $urls;
+
+ }
+
+
+ // MOCKABLES
+
+ /**
+ * This is just to make method mocks in the unit tests easier.
+ *
+ * @param string $param Option key to get
+ *
+ * @return mixed
+ *
+ * @codeCoverageIgnore
+ */
+ public function wp_get_option( $param ) {
+ return get_option( $param );
+ }
+
+ /**
+ * This is just to make method mocks in the unit tests easier.
+ *
+ * @param string $option_name Option name to be added
+ * @param mixed $option_value Option value
+ *
+ * @return mixed
+ *
+ * @codeCoverageIgnore
+ */
+ public function wp_add_option( $option_name, $option_value ) {
+ return add_option( $option_name, $option_value );
+ }
+
+ /**
+ * This is just to make method mocks in the unit tests easier.
+ *
+ * @param string $option_name Option name to be updated
+ * @param mixed $option_value Option value
+ *
+ * @return mixed
+ *
+ * @codeCoverageIgnore
+ */
+ public function wp_update_option( $option_name, $option_value ) {
+ return update_option( $option_name, $option_value );
+ }
+}
diff --git a/plugins/jetpack/modules/widgets/wordpress-post-widget/style.css b/plugins/jetpack/modules/widgets/wordpress-post-widget/style.css
new file mode 100644
index 00000000..651ec153
--- /dev/null
+++ b/plugins/jetpack/modules/widgets/wordpress-post-widget/style.css
@@ -0,0 +1,24 @@
+.jetpack-display-remote-posts {
+ margin: 5px 0 20px 0;
+}
+
+.jetpack-display-remote-posts h4 {
+ font-size: 90%;
+ margin: 5px 0;
+ padding: 0;
+}
+
+.jetpack-display-remote-posts h4 a {
+ text-decoration: none;
+}
+
+.jetpack-display-remote-posts p {
+ margin: 0 !important;
+ padding: 0;
+ line-height: 1.4em !important;
+ font-size: 90%;
+}
+
+.jetpack-display-remote-posts img {
+ max-width: 100%;
+}
diff --git a/plugins/jetpack/modules/woocommerce-analytics/classes/wp-woocommerce-analytics-universal.php b/plugins/jetpack/modules/woocommerce-analytics/classes/wp-woocommerce-analytics-universal.php
new file mode 100644
index 00000000..5214db3f
--- /dev/null
+++ b/plugins/jetpack/modules/woocommerce-analytics/classes/wp-woocommerce-analytics-universal.php
@@ -0,0 +1,399 @@
+<?php
+/**
+ * Jetpack_WooCommerce_Analytics_Universal
+ *
+ * @package Jetpack
+ * @author Automattic
+ */
+
+/**
+ * Bail if accessed directly
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class Jetpack_WooCommerce_Analytics_Universal
+ * Filters and Actions added to Store pages to perform analytics
+ */
+class Jetpack_WooCommerce_Analytics_Universal {
+ /**
+ * Jetpack_WooCommerce_Analytics_Universal constructor.
+ */
+ public function __construct() {
+ // loading _wca
+ add_action( 'wp_head', array( $this, 'wp_head_top' ), 1 );
+
+ // add to carts from non-product pages or lists (search, store etc.)
+ add_action( 'wp_head', array( $this, 'loop_session_events' ), 2 );
+
+ // loading s.js
+ add_action( 'wp_head', array( $this, 'wp_head_bottom' ), 999999 );
+
+ // Capture cart events
+ add_action( 'woocommerce_add_to_cart', array( $this, 'capture_add_to_cart' ), 10, 6 );
+
+ // single product page view
+ add_action( 'woocommerce_after_single_product', array( $this, 'capture_product_view' ) );
+
+ add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart' ) );
+ add_action( 'woocommerce_after_mini_cart', array( $this, 'remove_from_cart' ) );
+ add_action( 'wcct_before_cart_widget', array( $this, 'remove_from_cart' ) );
+ add_filter( 'woocommerce_cart_item_remove_link', array( $this, 'remove_from_cart_attributes' ), 10, 2 );
+
+ // cart checkout
+ add_action( 'woocommerce_after_checkout_form', array( $this, 'checkout_process' ) );
+
+ // order confirmed
+ add_action( 'woocommerce_thankyou', array( $this, 'order_process' ), 10, 1 );
+ add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart_via_quantity' ), 10, 1 );
+ }
+
+ /**
+ * Make _wca available to queue events
+ */
+ public function wp_head_top() {
+ if ( is_cart() || is_checkout() || is_checkout_pay_page() || is_order_received_page() || is_add_payment_method_page() ) {
+ $prevent_referrer_code = '<script>window._wca_prevent_referrer = true;</script>';
+ echo "$prevent_referrer_code\r\n";
+ }
+ $wca_code = '<script>window._wca = window._wca || [];</script>';
+ echo "$wca_code\r\n";
+ }
+
+
+ /**
+ * Place script to call s.js, Store Analytics
+ */
+ public function wp_head_bottom() {
+ $filename = 's-' . gmdate( 'YW' ) . '.js';
+ $async_code = "<script async src='https://stats.wp.com/" . $filename . "'></script>";
+ echo "$async_code\r\n";
+ }
+
+ /**
+ * On product lists or other non-product pages, add an event listener to "Add to Cart" button click
+ */
+ public function loop_session_events() {
+ $blogid = Jetpack::get_option( 'id' );
+
+ // check for previous add-to-cart cart events
+ if ( is_object( WC()->session ) ) {
+ $data = WC()->session->get( 'wca_session_data' );
+ if ( ! empty( $data ) ) {
+ foreach ( $data as $data_instance ) {
+ $product = wc_get_product( $data_instance['product_id'] );
+ if ( ! $product ) {
+ continue;
+ }
+ $product_details = $this->get_product_details( $product );
+ wc_enqueue_js(
+ "_wca.push( {
+ '_en': '" . esc_js( $data_instance['event'] ) . "',
+ 'blog_id': '" . esc_js( $blogid ) . "',
+ 'pi': '" . esc_js( $data_instance['product_id'] ) . "',
+ 'pn': '" . esc_js( $product_details['name'] ) . "',
+ 'pc': '" . esc_js( $product_details['category'] ) . "',
+ 'pp': '" . esc_js( $product_details['price'] ) . "',
+ 'pq': '" . esc_js( $data_instance['quantity'] ) . "',
+ 'pt': '" . esc_js( $product_details['type'] ) . "',
+ 'ui': '" . esc_js( $this->get_user_id() ) . "',
+ } );"
+ );
+ }
+ // clear data
+ WC()->session->set( 'wca_session_data', '' );
+ }
+ }
+ }
+
+ /**
+ * On the cart page, add an event listener for removal of product click
+ */
+ public function remove_from_cart() {
+
+ // We listen at div.woocommerce because the cart 'form' contents get forcibly
+ // updated and subsequent removals from cart would then not have this click
+ // handler attached.
+ $blogid = Jetpack::get_option( 'id' );
+ wc_enqueue_js(
+ "jQuery( 'div.woocommerce' ).on( 'click', 'a.remove', function() {
+ var productID = jQuery( this ).data( 'product_id' );
+ var quantity = jQuery( this ).parent().parent().find( '.qty' ).val()
+ var productDetails = {
+ 'id': productID,
+ 'quantity': quantity ? quantity : '1',
+ };
+ _wca.push( {
+ '_en': 'woocommerceanalytics_remove_from_cart',
+ 'blog_id': '" . esc_js( $blogid ) . "',
+ 'pi': productDetails.id,
+ 'pq': productDetails.quantity,
+ 'ui': '" . esc_js( $this->get_user_id() ) . "',
+ } );
+ } );"
+ );
+ }
+
+ /**
+ * Adds the product ID to the remove product link (for use by remove_from_cart above) if not present
+ *
+ * @param string $url Full HTML a tag of the link to remove an item from the cart.
+ * @param string $key Unique Key ID for a cart item.
+ *
+ * @return mixed.
+ */
+ public function remove_from_cart_attributes( $url, $key ) {
+ if ( false !== strpos( $url, 'data-product_id' ) ) {
+ return $url;
+ }
+
+ $item = WC()->cart->get_cart_item( $key );
+ $product = $item['data'];
+
+ $new_attributes = sprintf(
+ '" data-product_id="%s">',
+ esc_attr( $product->get_id() )
+ );
+
+ $url = str_replace( '">', $new_attributes, $url );
+ return $url;
+ }
+
+ /**
+ * Gather relevant product information
+ *
+ * @param array $product product
+ * @return array
+ */
+ public function get_product_details( $product ) {
+ return array(
+ 'id' => $product->get_id(),
+ 'name' => $product->get_title(),
+ 'category' => $this->get_product_categories_concatenated( $product ),
+ 'price' => $product->get_price(),
+ 'type' => $product->get_type(),
+ );
+ }
+
+ /**
+ * Track a product page view
+ */
+ public function capture_product_view() {
+
+ global $product;
+ $blogid = Jetpack::get_option( 'id' );
+ $product_details = $this->get_product_details( $product );
+
+ wc_enqueue_js(
+ "_wca.push( {
+ '_en': 'woocommerceanalytics_product_view',
+ 'blog_id': '" . esc_js( $blogid ) . "',
+ 'pi': '" . esc_js( $product_details['id'] ) . "',
+ 'pn': '" . esc_js( $product_details['name'] ) . "',
+ 'pc': '" . esc_js( $product_details['category'] ) . "',
+ 'pp': '" . esc_js( $product_details['price'] ) . "',
+ 'pt': '" . esc_js( $product_details['type'] ) . "',
+ 'ui': '" . esc_js( $this->get_user_id() ) . "',
+ } );"
+ );
+ }
+
+ /**
+ * On the Checkout page, trigger an event for each product in the cart
+ */
+ public function checkout_process() {
+
+ $universal_commands = array();
+ $cart = WC()->cart->get_cart();
+ $blogid = Jetpack::get_option( 'id' );
+
+ foreach ( $cart as $cart_item_key => $cart_item ) {
+ /**
+ * This filter is already documented in woocommerce/templates/cart/cart.php
+ */
+ $product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
+
+ if ( ! $product ) {
+ continue;
+ }
+
+ $product_details = $this->get_product_details( $product );
+
+ $universal_commands[] = "_wca.push( {
+ '_en': 'woocommerceanalytics_product_checkout',
+ 'blog_id': '" . esc_js( $blogid ) . "',
+ 'pi': '" . esc_js( $product_details['id'] ) . "',
+ 'pn': '" . esc_js( $product_details['name'] ) . "',
+ 'pc': '" . esc_js( $product_details['category'] ) . "',
+ 'pp': '" . esc_js( $product_details['price'] ) . "',
+ 'pq': '" . esc_js( $cart_item['quantity'] ) . "',
+ 'pt': '" . esc_js( $product_details['type'] ) . "',
+ 'ui': '" . esc_js( $this->get_user_id() ) . "',
+ } );";
+ }
+
+ wc_enqueue_js( implode( "\r\n", $universal_commands ) );
+ }
+
+ /**
+ * After the checkout process, fire an event for each item in the order
+ *
+ * @param string $order_id Order Id.
+ */
+ public function order_process( $order_id ) {
+ $order = wc_get_order( $order_id );
+ $universal_commands = array();
+ $blogid = Jetpack::get_option( 'id' );
+
+ // loop through products in the order and queue a purchase event.
+ foreach ( $order->get_items() as $order_item_id => $order_item ) {
+ $product = $order->get_product_from_item( $order_item );
+
+ $product_details = $this->get_product_details( $product );
+
+ $universal_commands[] = "_wca.push( {
+ '_en': 'woocommerceanalytics_product_purchase',
+ 'blog_id': '" . esc_js( $blogid ) . "',
+ 'pi': '" . esc_js( $product_details['id'] ) . "',
+ 'pn': '" . esc_js( $product_details['name'] ) . "',
+ 'pc': '" . esc_js( $product_details['category'] ) . "',
+ 'pp': '" . esc_js( $product_details['price'] ) . "',
+ 'pq': '" . esc_js( $order_item->get_quantity() ) . "',
+ 'pt': '" . esc_js( $product_details['type'] ) . "',
+ 'oi': '" . esc_js( $order->get_order_number() ) . "',
+ 'ui': '" . esc_js( $this->get_user_id() ) . "',
+ } );";
+ }
+
+ wc_enqueue_js( implode( "\r\n", $universal_commands ) );
+ }
+
+ /**
+ * Listen for clicks on the "Update Cart" button to know if an item has been removed by
+ * updating its quantity to zero
+ */
+ public function remove_from_cart_via_quantity() {
+ $blogid = Jetpack::get_option( 'id' );
+
+ wc_enqueue_js(
+ "
+ jQuery( 'button[name=update_cart]' ).on( 'click', function() {
+ var cartItems = jQuery( '.cart_item' );
+ cartItems.each( function( item ) {
+ var qty = jQuery( this ).find( 'input.qty' );
+ if ( qty && qty.val() === '0' ) {
+ var productID = jQuery( this ).find( '.product-remove a' ).data( 'product_id' );
+ _wca.push( {
+ '_en': 'woocommerceanalytics_remove_from_cart',
+ 'blog_id': '" . esc_js( $blogid ) . "',
+ 'pi': productID,
+ 'ui': '" . esc_js( $this->get_user_id() ) . "',
+ } );
+ }
+ } );
+ } );
+ "
+ );
+ }
+
+ /**
+ * Get the current user id
+ *
+ * @return int
+ */
+ public function get_user_id() {
+ if ( is_user_logged_in() ) {
+ $blogid = Jetpack::get_option( 'id' );
+ $userid = get_current_user_id();
+ return $blogid . ':' . $userid;
+ }
+ return 'null';
+ }
+
+ /**
+ * @param $cart_item_key
+ * @param $product_id
+ * @param $quantity
+ * @param $variation_id
+ * @param $variation
+ * @param $cart_item_data
+ */
+ public function capture_add_to_cart( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
+ $referer_postid = isset( $_SERVER['HTTP_REFERER'] ) ? url_to_postid( $_SERVER['HTTP_REFERER'] ) : 0;
+ // if the referring post is not a product OR the product being added is not the same as post
+ // (eg. related product list on single product page) then include a product view event
+ if ( ! wc_get_product( $referer_postid ) || $product_id != $referer_postid ) {
+ $this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_product_view' );
+ }
+ // add cart event to the session data
+ $this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_add_to_cart' );
+ }
+
+ /**
+ * @param $product_id
+ * @param $quantity
+ * @param $event
+ */
+ public function capture_event_in_session_data( $product_id, $quantity, $event ) {
+
+ $product = wc_get_product( $product_id );
+ if ( ! $product ) {
+ return;
+ }
+
+ $quantity = ( $quantity == 0 ) ? 1 : $quantity;
+
+ // check for existing data
+ if ( is_object( WC()->session ) ) {
+ $data = WC()->session->get( 'wca_session_data' );
+ if ( empty( $data ) || ! is_array( $data ) ) {
+ $data = array();
+ }
+ } else {
+ $data = array();
+ }
+
+ // extract new event data
+ $new_data = array(
+ 'event' => $event,
+ 'product_id' => (string) $product_id,
+ 'quantity' => (string) $quantity,
+ );
+
+ // append new data
+ $data[] = $new_data;
+
+ WC()->session->set( 'wca_session_data', $data );
+ }
+
+ /**
+ * Gets product categories or varation attributes as a formatted concatenated string
+ *
+ * @param object $product WC_Product.
+ * @return string
+ */
+ public function get_product_categories_concatenated( $product ) {
+
+ if ( ! $product ) {
+ return '';
+ }
+
+ $variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : '';
+ if ( is_array( $variation_data ) && ! empty( $variation_data ) ) {
+ $line = wc_get_formatted_variation( $variation_data, true );
+ } else {
+ $out = array();
+ $categories = get_the_terms( $product->get_id(), 'product_cat' );
+ if ( $categories ) {
+ foreach ( $categories as $category ) {
+ $out[] = $category->name;
+ }
+ }
+ $line = join( '/', $out );
+ }
+ return $line;
+ }
+
+}
diff --git a/plugins/jetpack/modules/woocommerce-analytics/wp-woocommerce-analytics.php b/plugins/jetpack/modules/woocommerce-analytics/wp-woocommerce-analytics.php
new file mode 100644
index 00000000..d078af40
--- /dev/null
+++ b/plugins/jetpack/modules/woocommerce-analytics/wp-woocommerce-analytics.php
@@ -0,0 +1,94 @@
+<?php
+/**
+ * Jetpack_WooCommerce_Analytics is ported from the Jetpack_Google_Analytics code.
+ *
+ * @package Jetpack
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+require_once plugin_basename( 'classes/wp-woocommerce-analytics-universal.php' );
+
+/**
+ * Class Jetpack_WooCommerce_Analytics
+ * Instantiate WooCommerce Analytics
+ */
+class Jetpack_WooCommerce_Analytics {
+
+ /**
+ * Instance of this class
+ *
+ * @var Jetpack_WooCommerce_Analytics - Static property to hold our singleton instance
+ */
+ private static $instance = false;
+
+ /**
+ * Instance of the Universal functions
+ *
+ * @var Static property to hold concrete analytics impl that does the work (universal or legacy)
+ */
+ private static $analytics = false;
+
+ /**
+ * WooCommerce Analytics is only available to Jetpack connected WooCommerce stores with both plugins set to active
+ * and WooCommerce version 3.0 or higher
+ *
+ * @return bool
+ */
+ public static function shouldTrackStore() {
+ /**
+ * Make sure WooCommerce is installed and active
+ *
+ * This action is documented in https://docs.woocommerce.com/document/create-a-plugin
+ */
+ if ( ! in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', Jetpack::get_active_plugins() ) ) ) {
+ return false;
+ }
+ // Tracking only Site pages
+ if ( is_admin() ) {
+ return false;
+ }
+ // Don't track site admins
+ if ( is_user_logged_in() && in_array( 'administrator', wp_get_current_user()->roles ) ) {
+ return false;
+ }
+ // Make sure Jetpack is installed and active
+ if ( ! Jetpack::is_active() ) {
+ return false;
+ }
+ // Ensure the WooCommerce class exists and is a valid version
+ $minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( WC_VERSION, '3.0', '>=' );
+ if ( ! $minimum_woocommerce_active ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * This is our constructor, which is private to force the use of get_instance()
+ *
+ * @return void
+ */
+ private function __construct() {
+ $analytics = new Jetpack_WooCommerce_Analytics_Universal();
+ }
+
+ /**
+ * Function to instantiate our class and make it a singleton
+ */
+ public static function get_instance() {
+ if ( ! self::shouldTrackStore() ) {
+ return;
+ }
+ if ( ! self::$instance ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+}
+
+global $jetpack_woocommerce_analytics;
+$jetpack_woocommerce_analytics = Jetpack_WooCommerce_Analytics::get_instance();
diff --git a/plugins/jetpack/modules/wordads.php b/plugins/jetpack/modules/wordads.php
new file mode 100644
index 00000000..3b3a9884
--- /dev/null
+++ b/plugins/jetpack/modules/wordads.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Module Name: Ads
+ * Module Description: Earn income by allowing Jetpack to display high quality ads.
+ * Sort Order: 1
+ * First Introduced: 4.5.0
+ * Requires Connection: Yes
+ * Auto Activate: No
+ * Module Tags: Traffic, Appearance
+ * Additional Search Queries: advertising, ad codes, ads
+ * Plans: premium, business
+ */
+
+function jetpack_load_wordads() {
+ Jetpack::enable_module_configurable( __FILE__ );
+ require_once( dirname( __FILE__ ) . "/wordads/wordads.php" );
+}
+
+jetpack_load_wordads();
diff --git a/plugins/jetpack/modules/wordads/css/style.css b/plugins/jetpack/modules/wordads/css/style.css
new file mode 100644
index 00000000..687334d6
--- /dev/null
+++ b/plugins/jetpack/modules/wordads/css/style.css
@@ -0,0 +1,63 @@
+/**
+ HTML markup structure of an ad:
+
+ <div class="wpcnt">
+ <div class="wpa [wpmrec|wpwidesky|wpleaderboard]">
+ <a class="wpa-about" href="http://wordpress.com/about-these-ads/" rel="nofollow">
+ About these ads
+ </a>
+ <div class="u">
+ [ad unit here]
+ </div>
+ </div>
+ </div>
+*/
+
+/* outer container */
+.wpcnt {
+ text-align: center;
+ line-height: 2;
+}
+
+/* inner container */
+.wpa {
+ position: relative;
+ overflow: hidden; /* this hides "about these ads" when there's no adfill */
+ display: inline-block;
+ max-width: 100%; /* important! this bit of CSS will *crop* any ad that's larger than the parent container! */
+}
+
+/* about these ads */
+.wpa-about {
+ position: absolute;
+ top: 5px;
+ left: 0;
+ right: 0;
+ display: block;
+ margin-top: 0;
+ color: #888;
+ font: 10px/1 "Open Sans", Arial, sans-serif !important;
+ text-align: left !important;
+ text-decoration: none !important;
+ opacity: 0.85;
+ border-bottom: none !important; /* some themes ad dotted underlines, that won't look nice */
+ box-shadow: none !important;
+}
+
+/* ad unit wrapper */
+.wpa .u>div { /* @todo: deprecate wpdvert */
+ display: block;
+ margin-top: 5px; /* this makes "about these ads" visible */
+ margin-bottom: 1em; /* every ad should have a little space below it */
+}
+
+div.wpa>div {
+ margin-top: 20px;
+}
+
+.wpa .u .adsbygoogle {
+ display: block;
+ margin-top: 17px; /* this makes "about these ads" visible */
+ margin-bottom: 1em; /* every ad should have a little space below it */
+ background-color: transparent;
+}
diff --git a/plugins/jetpack/modules/wordads/php/admin.php b/plugins/jetpack/modules/wordads/php/admin.php
new file mode 100644
index 00000000..b7071666
--- /dev/null
+++ b/plugins/jetpack/modules/wordads/php/admin.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * The standard set of admin pages for the user if Jetpack is installed
+ */
+class WordAds_Admin {
+
+ /**
+ * @since 4.5.0
+ */
+ function __construct() {
+ global $wordads;
+
+ if ( current_user_can( 'manage_options' ) && isset( $_GET['ads_debug'] ) ) {
+ WordAds_API::update_wordads_status_from_api();
+ add_action( 'admin_notices', array( $this, 'debug_output' ) );
+ }
+ }
+
+ /**
+ * Output the API connection debug
+ *
+ * @since 4.5.0
+ */
+ function debug_output() {
+ global $wordads, $wordads_status_response;
+ $response = $wordads_status_response;
+ if ( empty( $response ) ) {
+ $response = 'No response from API :(';
+ } else {
+ $response = print_r( $response, 1 );
+ }
+
+ $status = $wordads->option( 'wordads_approved' ) ?
+ '<span style="color:green;">Yes</span>' :
+ '<span style="color:red;">No</span>';
+
+ $type = $wordads->option( 'wordads_approved' ) ? 'updated' : 'error';
+ echo <<<HTML
+ <div class="notice $type is-dismissible">
+ <p>Status: $status</p>
+ <pre>$response</pre>
+ </div>
+HTML;
+ }
+}
+
+global $wordads_admin;
+$wordads_admin = new WordAds_Admin();
diff --git a/plugins/jetpack/modules/wordads/php/api.php b/plugins/jetpack/modules/wordads/php/api.php
new file mode 100644
index 00000000..34b5235a
--- /dev/null
+++ b/plugins/jetpack/modules/wordads/php/api.php
@@ -0,0 +1,143 @@
+<?php
+
+/**
+ * Methods for accessing data through the WPCOM REST API
+ *
+ * @since 4.5.0
+ */
+class WordAds_API {
+
+ private static $wordads_status = null;
+
+ /**
+ * Returns site's WordAds status
+ *
+ * @return array boolean values for 'approved' and 'active'
+ *
+ * @since 4.5.0
+ */
+ public static function get_wordads_status() {
+ global $wordads_status_response;
+ if ( Jetpack::is_development_mode() ) {
+ self::$wordads_status = array(
+ 'approved' => true,
+ 'active' => true,
+ 'house' => true,
+ 'unsafe' => false,
+ );
+
+ return self::$wordads_status;
+ }
+
+ $endpoint = sprintf( '/sites/%d/wordads/status', Jetpack::get_option( 'id' ) );
+ $wordads_status_response = $response = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint );
+ if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ return new WP_Error( 'api_error', __( 'Error connecting to API.', 'jetpack' ), $response );
+ }
+
+ $body = json_decode( wp_remote_retrieve_body( $response ) );
+ self::$wordads_status = array(
+ 'approved' => $body->approved,
+ 'active' => $body->active,
+ 'house' => $body->house,
+ 'unsafe' => $body->unsafe,
+ );
+
+ return self::$wordads_status;
+ }
+
+ /**
+ * Returns the ads.txt content needed to run WordAds.
+ *
+ * @return array string contents of the ads.txt file.
+ *
+ * @since 6.1.0
+ */
+ public static function get_wordads_ads_txt() {
+ $endpoint = sprintf( '/sites/%d/wordads/ads-txt', Jetpack::get_option( 'id' ) );
+ $wordads_status_response = $response = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint );
+ if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ return new WP_Error( 'api_error', __( 'Error connecting to API.', 'jetpack' ), $response );
+ }
+
+ $body = json_decode( wp_remote_retrieve_body( $response ) );
+ $ads_txt = str_replace( '\\n', PHP_EOL, $body->adstxt );
+ return $ads_txt;
+ }
+
+ /**
+ * Returns status of WordAds approval.
+ *
+ * @return boolean true if site is WordAds approved
+ *
+ * @since 4.5.0
+ */
+ public static function is_wordads_approved() {
+ if ( is_null( self::$wordads_status ) ) {
+ self::get_wordads_status();
+ }
+
+ return self::$wordads_status['approved'] ? '1' : '0';
+ }
+
+ /**
+ * Returns status of WordAds active.
+ *
+ * @return boolean true if ads are active on site
+ *
+ * @since 4.5.0
+ */
+ public static function is_wordads_active() {
+ if ( is_null( self::$wordads_status ) ) {
+ self::get_wordads_status();
+ }
+
+ return self::$wordads_status['active'] ? '1' : '0';
+ }
+
+ /**
+ * Returns status of WordAds house ads.
+ *
+ * @return boolean true if WP.com house ads should be shown
+ *
+ * @since 4.5.0
+ */
+ public static function is_wordads_house() {
+ if ( is_null( self::$wordads_status ) ) {
+ self::get_wordads_status();
+ }
+
+ return self::$wordads_status['house'] ? '1' : '0';
+ }
+
+
+ /**
+ * Returns whether or not this site is safe to run ads on.
+ *
+ * @return boolean true if ads shown not be shown on this site.
+ *
+ * @since 6.5.0
+ */
+ public static function is_wordads_unsafe() {
+ if ( is_null( self::$wordads_status ) ) {
+ self::get_wordads_status();
+ }
+
+ return self::$wordads_status['unsafe'] ? '1' : '0';
+ }
+
+ /**
+ * Grab WordAds status from WP.com API and store as option
+ *
+ * @since 4.5.0
+ */
+ static function update_wordads_status_from_api() {
+ $status = self::get_wordads_status();
+ if ( ! is_wp_error( $status ) ) {
+ update_option( 'wordads_approved', self::is_wordads_approved(), true );
+ update_option( 'wordads_active', self::is_wordads_active(), true );
+ update_option( 'wordads_house', self::is_wordads_house(), true );
+ update_option( 'wordads_unsafe', self::is_wordads_unsafe(), true );
+ }
+ }
+}
diff --git a/plugins/jetpack/modules/wordads/php/cron.php b/plugins/jetpack/modules/wordads/php/cron.php
new file mode 100644
index 00000000..88677e53
--- /dev/null
+++ b/plugins/jetpack/modules/wordads/php/cron.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * WordAds cron tasks
+ *
+ * @since 4.5.0
+ */
+class WordAds_Cron {
+
+ /**
+ * Add the actions the cron tasks will use
+ *
+ * @since 4.5.0
+ */
+ function __construct() {
+ add_action( 'wordads_cron_status', array( $this, 'update_wordads_status' ) );
+ }
+
+ /**
+ * Registered scheduled events on activation
+ *
+ * @since 4.5.0
+ */
+ static function activate() {
+ wp_schedule_event( time(), 'daily', 'wordads_cron_status' );
+ }
+
+ /**
+ * Clear scheduled hooks on deactivation
+ *
+ * @since 4.5.0
+ */
+ static function deactivate() {
+ wp_clear_scheduled_hook( 'wordads_cron_status' );
+ }
+
+ /**
+ * Grab WordAds status from WP.com API
+ *
+ * @since 4.5.0
+ */
+ static function update_wordads_status() {
+ WordAds_API::update_wordads_status_from_api();
+ }
+}
+
+global $wordads_cron;
+$wordads_cron = new WordAds_Cron();
diff --git a/plugins/jetpack/modules/wordads/php/networks/amazon.php b/plugins/jetpack/modules/wordads/php/networks/amazon.php
new file mode 100644
index 00000000..19f53fbe
--- /dev/null
+++ b/plugins/jetpack/modules/wordads/php/networks/amazon.php
@@ -0,0 +1,3 @@
+<?php
+
+// stub
diff --git a/plugins/jetpack/modules/wordads/php/params.php b/plugins/jetpack/modules/wordads/php/params.php
new file mode 100644
index 00000000..ef3f3326
--- /dev/null
+++ b/plugins/jetpack/modules/wordads/php/params.php
@@ -0,0 +1,226 @@
+<?php
+
+class WordAds_Params {
+
+ /**
+ * Setup parameters for serving the ads
+ *
+ * @since 4.5.0
+ */
+ public function __construct() {
+ // WordAds setting => default
+ $settings = array(
+ 'wordads_approved' => false,
+ 'wordads_active' => false,
+ 'wordads_house' => true,
+ 'wordads_unsafe' => false,
+ 'enable_header_ad' => true,
+ 'wordads_second_belowpost' => true,
+ 'wordads_display_front_page' => true,
+ 'wordads_display_post' => true,
+ 'wordads_display_page' => true,
+ 'wordads_display_archive' => true,
+ 'wordads_custom_adstxt' => '',
+ );
+
+ // grab settings, or set as default if it doesn't exist
+ $this->options = array();
+ foreach ( $settings as $setting => $default ) {
+ $option = get_option( $setting, null );
+
+ if ( is_null( $option ) ) {
+ update_option( $setting, $default, true );
+ $option = $default;
+ }
+
+ $this->options[ $setting ] = 'wordads_custom_adstxt' !== $setting ? (bool) $option : $option;
+ }
+
+ $host = 'localhost';
+ if ( isset( $_SERVER['HTTP_HOST'] ) ) {
+ $host = $_SERVER['HTTP_HOST'];
+ }
+
+ $this->url = ( is_ssl() ? 'https' : 'http' ) . '://' . $host . $_SERVER['REQUEST_URI'];
+ if ( ! ( false === strpos( $this->url, '?' ) ) && ! isset( $_GET['p'] ) ) {
+ $this->url = substr( $this->url, 0, strpos( $this->url, '?' ) );
+ }
+
+ $this->cloudflare = self::is_cloudflare();
+ $this->blog_id = Jetpack::get_option( 'id', 0 );
+ $this->mobile_device = jetpack_is_mobile( 'any', true );
+ $this->targeting_tags = array(
+ 'WordAds' => 1,
+ 'BlogId' => Jetpack::is_development_mode() ? 0 : Jetpack_Options::get_option( 'id' ),
+ 'Domain' => esc_js( parse_url( home_url(), PHP_URL_HOST ) ),
+ 'PageURL' => esc_js( $this->url ),
+ 'LangId' => false !== strpos( get_bloginfo( 'language' ), 'en' ) ? 1 : 0, // TODO something else?
+ 'AdSafe' => 1, // TODO
+ );
+ }
+
+ /**
+ * @return boolean true if the user is browsing on a mobile device (iPad not included)
+ *
+ * @since 4.5.0
+ */
+ public function is_mobile() {
+ return ! empty( $this->mobile_device );
+ }
+
+ /**
+ * @return boolean true if site is being served via CloudFlare
+ *
+ * @since 4.5.0
+ */
+ public static function is_cloudflare() {
+ if (
+ defined( 'WORDADS_CLOUDFLARE' )
+ || isset( $_SERVER['HTTP_CF_CONNECTING_IP'] )
+ || isset( $_SERVER['HTTP_CF_IPCOUNTRY'] )
+ || isset( $_SERVER['HTTP_CF_VISITOR'] )
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return boolean true if user is browsing in iOS device
+ *
+ * @since 4.5.0
+ */
+ public function is_ios() {
+ return in_array( $this->get_device(), array( 'ipad', 'iphone', 'ipod' ) );
+ }
+
+ /**
+ * Returns the user's device (see user-agent.php) or 'desktop'
+ *
+ * @return string user device
+ *
+ * @since 4.5.0
+ */
+ public function get_device() {
+ global $agent_info;
+
+ if ( ! empty( $this->mobile_device ) ) {
+ return $this->mobile_device;
+ }
+
+ if ( $agent_info->is_ipad() ) {
+ return 'ipad';
+ }
+
+ return 'desktop';
+ }
+
+ /**
+ * @return string The type of page that is being loaded
+ *
+ * @since 4.5.0
+ */
+ public function get_page_type() {
+ if ( ! empty( $this->page_type ) ) {
+ return $this->page_type;
+ }
+
+ if ( self::is_static_home() ) {
+ $this->page_type = 'static_home';
+ } elseif ( is_home() ) {
+ $this->page_type = 'home';
+ } elseif ( is_page() ) {
+ $this->page_type = 'page';
+ } elseif ( is_single() ) {
+ $this->page_type = 'post';
+ } elseif ( is_search() ) {
+ $this->page_type = 'search';
+ } elseif ( is_category() ) {
+ $this->page_type = 'category';
+ } elseif ( is_archive() ) {
+ $this->page_type = 'archive';
+ } else {
+ $this->page_type = 'wtf';
+ }
+
+ return $this->page_type;
+ }
+
+ /**
+ * @return int The page type code for ipw config
+ *
+ * @since 5.6.0
+ */
+ public function get_page_type_ipw() {
+ if ( ! empty( $this->page_type_ipw ) ) {
+ return $this->page_type_ipw;
+ }
+
+ $page_type_ipw = 6;
+ if ( self::is_static_home() || is_home() || is_front_page() ) {
+ $page_type_ipw = 0;
+ } elseif ( is_page() ) {
+ $page_type_ipw = 2;
+ } elseif ( is_singular() ) {
+ $page_type_ipw = 1;
+ } elseif ( is_search() ) {
+ $page_type_ipw = 4;
+ } elseif ( is_category() || is_tag() || is_archive() || is_author() ) {
+ $page_type_ipw = 3;
+ } elseif ( is_404() ) {
+ $page_type_ipw = 5;
+ }
+
+ $this->page_type_ipw = $page_type_ipw;
+ return $page_type_ipw;
+ }
+
+ /**
+ * Returns true if page is static home
+ *
+ * @return boolean true if page is static home
+ *
+ * @since 4.5.0
+ */
+ public static function is_static_home() {
+ return is_front_page() &&
+ 'page' == get_option( 'show_on_front' ) &&
+ get_option( 'page_on_front' );
+ }
+
+ /**
+ * Logic for if we should show an ad
+ *
+ * @since 4.5.0
+ */
+ public function should_show() {
+ global $wp_query;
+ if ( ( is_front_page() || is_home() ) && ! $this->options['wordads_display_front_page'] ) {
+ return false;
+ }
+
+ if ( is_single() && ! $this->options['wordads_display_post'] ) {
+ return false;
+ }
+
+ if ( is_page() && ! $this->options['wordads_display_page'] ) {
+ return false;
+ }
+
+ if ( ( is_archive() || is_search() ) && ! $this->options['wordads_display_archive'] ) {
+ return false;
+ }
+
+ if ( is_single() || ( is_page() && ! is_home() ) ) {
+ return true;
+ }
+
+ // TODO this would be a good place for allowing the user to specify
+ if ( ( is_home() || is_archive() || is_search() ) && 0 == $wp_query->current_post ) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/jetpack/modules/wordads/php/widgets.php b/plugins/jetpack/modules/wordads/php/widgets.php
new file mode 100644
index 00000000..782c2dcd
--- /dev/null
+++ b/plugins/jetpack/modules/wordads/php/widgets.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * Widget for inserting an ad into your sidebar
+ *
+ * @since 4.5.0
+ */
+class WordAds_Sidebar_Widget extends WP_Widget {
+
+ private static $allowed_tags = array( 'mrec', 'wideskyscraper' );
+ private static $num_widgets = 0;
+
+ function __construct() {
+ parent::__construct(
+ 'wordads_sidebar_widget',
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
+ apply_filters( 'jetpack_widget_name', 'Ads' ),
+ array(
+ 'description' => __( 'Insert an ad unit wherever you can place a widget.', 'jetpack' ),
+ 'customize_selective_refresh' => true,
+ )
+ );
+ }
+
+ public function widget( $args, $instance ) {
+ global $wordads;
+ if ( $wordads->should_bail() ) {
+ return false;
+ }
+
+ if ( ! isset( $instance['unit'] ) ) {
+ $instance['unit'] = 'mrec';
+ }
+
+ self::$num_widgets++;
+ $about = __( 'Advertisements', 'jetpack' );
+ $width = WordAds::$ad_tag_ids[ $instance['unit'] ]['width'];
+ $height = WordAds::$ad_tag_ids[ $instance['unit'] ]['height'];
+ $unit_id = 1 == self::$num_widgets ? 3 : self::$num_widgets + 3; // 2nd belowpost is '4'
+ $section_id = 0 === $wordads->params->blog_id ?
+ WORDADS_API_TEST_ID :
+ $wordads->params->blog_id . $unit_id;
+
+ $snippet = '';
+ if ( $wordads->option( 'wordads_house', true ) ) {
+ $unit = 'mrec';
+ if ( 'leaderboard' == $instance['unit'] && ! $this->params->mobile_device ) {
+ $unit = 'leaderboard';
+ } elseif ( 'wideskyscraper' == $instance['unit'] ) {
+ $unit = 'widesky';
+ }
+
+ $snippet = $wordads->get_house_ad( $unit );
+ } else {
+ $snippet = $wordads->get_ad_snippet( $section_id, $height, $width, 'widget' );
+ }
+
+ echo <<< HTML
+ <div class="wpcnt">
+ <div class="wpa">
+ <span class="wpa-about">$about</span>
+ <div class="u {$instance['unit']}">
+ $snippet
+ </div>
+ </div>
+ </div>
+HTML;
+ }
+
+ public function form( $instance ) {
+ // ad unit type
+ if ( isset( $instance['unit'] ) ) {
+ $unit = $instance['unit'];
+ } else {
+ $unit = 'mrec';
+ }
+ ?>
+ <p>
+ <label for="<?php echo esc_attr( $this->get_field_id( 'unit' ) ); ?>"><?php _e( 'Tag Dimensions:', 'jetpack' ); ?></label>
+ <select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'unit' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'unit' ) ); ?>">
+ <?php
+ foreach ( WordAds::$ad_tag_ids as $ad_unit => $properties ) {
+ if ( ! in_array( $ad_unit, self::$allowed_tags ) ) {
+ continue;
+ }
+
+ $splits = explode( '_', $properties['tag'] );
+ $unit_pretty = "{$splits[0]} {$splits[1]}";
+ $selected = selected( $ad_unit, $unit, false );
+ echo "<option value='", esc_attr( $ad_unit ) ,"' ", $selected, '>', esc_html( $unit_pretty ) , '</option>';
+ }
+ ?>
+ </select>
+ </p>
+ <?php
+ }
+
+ public function update( $new_instance, $old_instance ) {
+ $instance = $old_instance;
+
+ if ( in_array( $new_instance['unit'], self::$allowed_tags ) ) {
+ $instance['unit'] = $new_instance['unit'];
+ } else {
+ $instance['unit'] = 'mrec';
+ }
+
+ return $instance;
+ }
+}
+
+function jetpack_wordads_widgets_init_callback() {
+ return register_widget( 'WordAds_Sidebar_Widget' );
+}
+
+add_action( 'widgets_init', 'jetpack_wordads_widgets_init_callback' );
diff --git a/plugins/jetpack/modules/wordads/wordads.php b/plugins/jetpack/modules/wordads/wordads.php
new file mode 100644
index 00000000..43a4e7d8
--- /dev/null
+++ b/plugins/jetpack/modules/wordads/wordads.php
@@ -0,0 +1,619 @@
+<?php
+
+define( 'WORDADS_ROOT', dirname( __FILE__ ) );
+define( 'WORDADS_BASENAME', plugin_basename( __FILE__ ) );
+define( 'WORDADS_FILE_PATH', WORDADS_ROOT . '/' . basename( __FILE__ ) );
+define( 'WORDADS_URL', plugins_url( '/', __FILE__ ) );
+define( 'WORDADS_API_TEST_ID', '26942' );
+define( 'WORDADS_API_TEST_ID2', '114160' );
+
+require_once WORDADS_ROOT . '/php/widgets.php';
+require_once WORDADS_ROOT . '/php/api.php';
+require_once WORDADS_ROOT . '/php/cron.php';
+
+class WordAds {
+
+ public $params = null;
+
+ public $ads = array();
+
+ /**
+ * Array of supported ad types.
+ *
+ * @var array
+ */
+ public static $ad_tag_ids = array(
+ 'mrec' => array(
+ 'tag' => '300x250_mediumrectangle',
+ 'height' => '250',
+ 'width' => '300',
+ ),
+ 'leaderboard' => array(
+ 'tag' => '728x90_leaderboard',
+ 'height' => '90',
+ 'width' => '728',
+ ),
+ 'mobile_leaderboard' => array(
+ 'tag' => '320x50_mobileleaderboard',
+ 'height' => '50',
+ 'width' => '320',
+ ),
+ 'wideskyscraper' => array(
+ 'tag' => '160x600_wideskyscraper',
+ 'height' => '600',
+ 'width' => '160',
+ ),
+ );
+
+ /**
+ * Mapping array of location slugs to placement ids
+ *
+ * @var array
+ */
+ public static $ad_location_ids = array(
+ 'top' => 110,
+ 'belowpost' => 120,
+ 'belowpost2' => 130,
+ 'sidebar' => 140,
+ 'widget' => 150,
+ 'gutenberg' => 200,
+ 'inline' => 310,
+ 'inline-plugin' => 320,
+ );
+
+ public static $SOLO_UNIT_CSS = 'float:left;margin-right:5px;margin-top:0px;';
+
+ /**
+ * Convenience function for grabbing options from params->options
+ *
+ * @param string $option the option to grab
+ * @param mixed $default (optional)
+ * @return option or $default if not set
+ *
+ * @since 4.5.0
+ */
+ function option( $option, $default = false ) {
+ if ( ! isset( $this->params->options[ $option ] ) ) {
+ return $default;
+ }
+
+ return $this->params->options[ $option ];
+ }
+
+ /**
+ * Returns the ad tag property array for supported ad types.
+ * @return array array with ad tags
+ *
+ * @since 7.1.0
+ */
+ function get_ad_tags() {
+ return self::$ad_tag_ids;
+ }
+
+ /**
+ * Returns the solo css for unit
+ * @return string the special css for solo units
+ *
+ * @since 7.1.0
+ */
+ function get_solo_unit_css() {
+ return self::$SOLO_UNIT_CSS;
+ }
+
+ /**
+ * Instantiate the plugin
+ *
+ * @since 4.5.0
+ */
+ function __construct() {
+ add_action( 'init', array( $this, 'init' ) );
+ }
+
+ /**
+ * Code to run on WordPress 'init' hook
+ *
+ * @since 4.5.0
+ */
+ function init() {
+ require_once WORDADS_ROOT . '/php/params.php';
+ $this->params = new WordAds_Params();
+
+ if ( $this->should_bail() || self::is_infinite_scroll() ) {
+ return;
+ }
+
+ if ( is_admin() ) {
+ require_once WORDADS_ROOT . '/php/admin.php';
+ return;
+ }
+
+ $this->insert_adcode();
+
+ if ( '/ads.txt' === $_SERVER['REQUEST_URI'] ) {
+
+ if ( false === ( $ads_txt_transient = get_transient( 'jetpack_ads_txt' ) ) ) {
+ $ads_txt_transient = ! is_wp_error( WordAds_API::get_wordads_ads_txt() ) ? WordAds_API::get_wordads_ads_txt() : '';
+ set_transient( 'jetpack_ads_txt', $ads_txt_transient, DAY_IN_SECONDS );
+ }
+
+ /**
+ * Provide plugins a way of modifying the contents of the automatically-generated ads.txt file.
+ *
+ * @module wordads
+ *
+ * @since 6.1.0
+ *
+ * @param string WordAds_API::get_wordads_ads_txt() The contents of the ads.txt file.
+ */
+ $ads_txt_content = apply_filters( 'wordads_ads_txt', $ads_txt_transient );
+
+ header( 'Content-Type: text/plain; charset=utf-8' );
+ echo esc_html( $ads_txt_content );
+ die();
+ }
+ }
+
+ /**
+ * Check for Jetpack's The_Neverending_Home_Page and use got_infinity
+ *
+ * @return boolean true if load came from infinite scroll
+ *
+ * @since 4.5.0
+ */
+ public static function is_infinite_scroll() {
+ return class_exists( 'The_Neverending_Home_Page' ) && The_Neverending_Home_Page::got_infinity();
+ }
+
+ /**
+ * Add the actions/filters to insert the ads. Checks for mobile or desktop.
+ *
+ * @since 4.5.0
+ */
+ private function insert_adcode() {
+ add_action( 'wp_head', array( $this, 'insert_head_meta' ), 20 );
+ add_action( 'wp_head', array( $this, 'insert_head_iponweb' ), 30 );
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+ add_filter( 'wordads_ads_txt', array( $this, 'insert_custom_adstxt' ) );
+
+ /**
+ * Filters enabling ads in `the_content` filter
+ *
+ * @see https://jetpack.com/support/ads/
+ *
+ * @module wordads
+ *
+ * @since 5.8.0
+ *
+ * @param bool True to disable ads in `the_content`
+ */
+ if ( ! apply_filters( 'wordads_content_disable', false ) ) {
+ add_filter( 'the_content', array( $this, 'insert_ad' ) );
+ }
+
+ /**
+ * Filters enabling ads in `the_excerpt` filter
+ *
+ * @see https://jetpack.com/support/ads/
+ *
+ * @module wordads
+ *
+ * @since 5.8.0
+ *
+ * @param bool True to disable ads in `the_excerpt`
+ */
+ if ( ! apply_filters( 'wordads_excerpt_disable', false ) ) {
+ add_filter( 'the_excerpt', array( $this, 'insert_ad' ) );
+ }
+
+ if ( $this->option( 'enable_header_ad', true ) ) {
+ switch ( get_stylesheet() ) {
+ case 'twentyseventeen':
+ case 'twentyfifteen':
+ case 'twentyfourteen':
+ add_action( 'wp_footer', array( $this, 'insert_header_ad_special' ) );
+ break;
+ default:
+ add_action( 'wp_head', array( $this, 'insert_header_ad' ), 100 );
+ break;
+ }
+ }
+ }
+
+ /**
+ * Register desktop scripts and styles
+ *
+ * @since 4.5.0
+ */
+ function enqueue_scripts() {
+ wp_enqueue_style(
+ 'wordads',
+ WORDADS_URL . 'css/style.css',
+ array(),
+ '2015-12-18'
+ );
+ }
+
+ /**
+ * IPONWEB metadata used by the various scripts
+ *
+ * @return [type] [description]
+ */
+ function insert_head_meta() {
+ $themename = esc_js( get_stylesheet() );
+ $pagetype = intval( $this->params->get_page_type_ipw() );
+ $data_tags = ( $this->params->cloudflare ) ? ' data-cfasync="false"' : '';
+ $site_id = $this->params->blog_id;
+ $consent = intval( isset( $_COOKIE['personalized-ads-consent'] ) );
+ echo <<<HTML
+ <script$data_tags type="text/javascript">
+ var __ATA_PP = { pt: $pagetype, ht: 2, tn: '$themename', amp: false, siteid: $site_id, consent: $consent };
+ var __ATA = __ATA || {};
+ __ATA.cmd = __ATA.cmd || [];
+ __ATA.criteo = __ATA.criteo || {};
+ __ATA.criteo.cmd = __ATA.criteo.cmd || [];
+ </script>
+HTML;
+ }
+
+ /**
+ * IPONWEB scripts in <head>
+ *
+ * @since 4.5.0
+ */
+ function insert_head_iponweb() {
+ $data_tags = ( $this->params->cloudflare ) ? ' data-cfasync="false"' : '';
+ echo <<<HTML
+ <link rel='dns-prefetch' href='//s.pubmine.com' />
+ <link rel='dns-prefetch' href='//x.bidswitch.net' />
+ <link rel='dns-prefetch' href='//static.criteo.net' />
+ <link rel='dns-prefetch' href='//ib.adnxs.com' />
+ <link rel='dns-prefetch' href='//aax.amazon-adsystem.com' />
+ <link rel='dns-prefetch' href='//bidder.criteo.com' />
+ <link rel='dns-prefetch' href='//cas.criteo.com' />
+ <link rel='dns-prefetch' href='//gum.criteo.com' />
+ <link rel='dns-prefetch' href='//ads.pubmatic.com' />
+ <link rel='dns-prefetch' href='//gads.pubmatic.com' />
+ <link rel='dns-prefetch' href='//tpc.googlesyndication.com' />
+ <link rel='dns-prefetch' href='//ad.doubleclick.net' />
+ <link rel='dns-prefetch' href='//googleads.g.doubleclick.net' />
+ <link rel='dns-prefetch' href='//www.googletagservices.com' />
+ <script$data_tags async type="text/javascript" src="//s.pubmine.com/head.js"></script>
+HTML;
+ }
+
+ /**
+ * Insert the ad onto the page
+ *
+ * @since 4.5.0
+ */
+ function insert_ad( $content ) {
+ // Don't insert ads in feeds, or for anything but the main display. (This is required for compatibility with the Publicize module).
+ if ( is_feed() || ! is_main_query() || ! in_the_loop() ) {
+ return $content;
+ }
+ /**
+ * Allow third-party tools to disable the display of in post ads.
+ *
+ * @module wordads
+ *
+ * @since 4.5.0
+ *
+ * @param bool true Should the in post unit be disabled. Default to false.
+ */
+ $disable = apply_filters( 'wordads_inpost_disable', false );
+ if ( ! $this->params->should_show() || $disable ) {
+ return $content;
+ }
+
+ $ad_type = $this->option( 'wordads_house' ) ? 'house' : 'iponweb';
+ return $content . $this->get_ad( 'belowpost', $ad_type );
+ }
+
+ /**
+ * Insert an inline ad into a post content
+ * Used for rendering the `wordads` shortcode.
+ *
+ * @since 6.1.0
+ */
+ function insert_inline_ad( $content ) {
+ // Ad JS won't work in XML feeds.
+ if ( is_feed() ) {
+ return $content;
+ }
+ /**
+ * Allow third-party tools to disable the display of in post ads.
+ *
+ * @module wordads
+ *
+ * @since 4.5.0
+ *
+ * @param bool true Should the in post unit be disabled. Default to false.
+ */
+ $disable = apply_filters( 'wordads_inpost_disable', false );
+ if ( $disable ) {
+ return $content;
+ }
+
+ $ad_type = $this->option( 'wordads_house' ) ? 'house' : 'iponweb';
+ $content .= $this->get_ad( 'inline', $ad_type );
+ return $content;
+ }
+
+ /**
+ * Inserts ad into header
+ *
+ * @since 4.5.0
+ */
+ function insert_header_ad() {
+ /**
+ * Allow third-party tools to disable the display of header ads.
+ *
+ * @module wordads
+ *
+ * @since 4.5.0
+ *
+ * @param bool true Should the header unit be disabled. Default to false.
+ */
+ if ( apply_filters( 'wordads_header_disable', false ) ) {
+ return;
+ }
+
+ $ad_type = $this->option( 'wordads_house' ) ? 'house' : 'iponweb';
+ echo $this->get_ad( 'top', $ad_type );
+ }
+
+ /**
+ * Special cases for inserting header unit via jQuery
+ *
+ * @since 4.5.0
+ */
+ function insert_header_ad_special() {
+ /**
+ * Allow third-party tools to disable the display of header ads.
+ *
+ * @module wordads
+ *
+ * @since 4.5.0
+ *
+ * @param bool true Should the header unit be disabled. Default to false.
+ */
+ if ( apply_filters( 'wordads_header_disable', false ) ) {
+ return;
+ }
+
+ $selector = '#content';
+ switch ( get_stylesheet() ) {
+ case 'twentyseventeen':
+ $selector = '#content';
+ break;
+ case 'twentyfifteen':
+ $selector = '#main';
+ break;
+ case 'twentyfourteen':
+ $selector = 'article:first';
+ break;
+ }
+
+ $ad_type = $this->option( 'wordads_house' ) ? 'house' : 'iponweb';
+ echo $this->get_ad( 'top', $ad_type );
+ echo <<<HTML
+ <script type="text/javascript">
+ jQuery('.wpcnt-header').insertBefore('$selector');
+ </script>
+HTML;
+ }
+
+ /**
+ * Filter the latest ads.txt to include custom user entries. Strips any tags or whitespace.
+ *
+ * @param string $adstxt The ads.txt being filtered
+ * @return string Filtered ads.txt with custom entries, if applicable
+ *
+ * @since 6.5.0
+ */
+ function insert_custom_adstxt( $adstxt ) {
+ $custom_adstxt = trim( wp_strip_all_tags( $this->option( 'wordads_custom_adstxt' ) ) );
+ if ( $custom_adstxt ) {
+ $adstxt .= "\n\n#Jetpack - User Custom Entries\n";
+ $adstxt .= $custom_adstxt . "\n";
+ }
+
+ return $adstxt;
+ }
+
+ /**
+ * Get the ad for the spot and type.
+ *
+ * @param string $spot top, side, inline, or belowpost
+ * @param string $type iponweb or adsense
+ */
+ function get_ad( $spot, $type = 'iponweb' ) {
+ $snippet = '';
+ if ( 'iponweb' == $type ) {
+ // Default to mrec
+ $width = 300;
+ $height = 250;
+
+ $section_id = WORDADS_API_TEST_ID;
+ $second_belowpost = '';
+ $snippet = '';
+ if ( 'top' == $spot ) {
+ // mrec for mobile, leaderboard for desktop
+ $section_id = 0 === $this->params->blog_id ? WORDADS_API_TEST_ID : $this->params->blog_id . '2';
+ $width = $this->params->mobile_device ? 300 : 728;
+ $height = $this->params->mobile_device ? 250 : 90;
+ $snippet = $this->get_ad_snippet( $section_id, $height, $width, $spot );
+ } elseif ( 'belowpost' == $spot ) {
+ $section_id = 0 === $this->params->blog_id ? WORDADS_API_TEST_ID : $this->params->blog_id . '1';
+ $width = 300;
+ $height = 250;
+
+ $snippet = $this->get_ad_snippet( $section_id, $height, $width, $spot, self::$SOLO_UNIT_CSS );
+ if ( $this->option( 'wordads_second_belowpost', true ) ) {
+ $section_id2 = 0 === $this->params->blog_id ? WORDADS_API_TEST_ID2 : $this->params->blog_id . '4';
+ $snippet .= $this->get_ad_snippet( $section_id2, $height, $width, $spot . '2', 'float:left;margin-top:0px;' );
+ }
+ } elseif ( 'inline' === $spot ) {
+ $section_id = 0 === $this->params->blog_id ? WORDADS_API_TEST_ID : $this->params->blog_id . '5';
+ $snippet = $this->get_ad_snippet( $section_id, $height, $width, $spot, self::$SOLO_UNIT_CSS );
+ }
+ } elseif ( 'house' == $type ) {
+ $leaderboard = 'top' == $spot && ! $this->params->mobile_device;
+ $snippet = $this->get_house_ad( $leaderboard ? 'leaderboard' : 'mrec' );
+ if ( 'belowpost' == $spot && $this->option( 'wordads_second_belowpost', true ) ) {
+ $snippet .= $this->get_house_ad( $leaderboard ? 'leaderboard' : 'mrec' );
+ }
+ }
+
+ return $this->get_ad_div( $spot, $snippet );
+ }
+
+
+ /**
+ * Returns the snippet to be inserted into the ad unit
+ *
+ * @param int $section_id
+ * @param int $height
+ * @param int $width
+ * @param int $location
+ * @param string $css
+ * @return string
+ *
+ * @since 5.7
+ */
+ function get_ad_snippet( $section_id, $height, $width, $location = '', $css = '' ) {
+ $this->ads[] = array(
+ 'location' => $location,
+ 'width' => $width,
+ 'height' => $height,
+ );
+ $ad_number = count( $this->ads ) . '-' . uniqid();
+
+ $data_tags = $this->params->cloudflare ? ' data-cfasync="false"' : '';
+ $css = esc_attr( $css );
+
+ $loc_id = 100;
+ if ( ! empty( self::$ad_location_ids[ $location ] ) ) {
+ $loc_id = self::$ad_location_ids[ $location ];
+ }
+
+ return <<<HTML
+ <div style="padding-bottom:15px;width:{$width}px;height:{$height}px;$css">
+ <div id="atatags-{$ad_number}">
+ <script$data_tags type="text/javascript">
+ __ATA.cmd.push(function() {
+ __ATA.initSlot('atatags-{$ad_number}', {
+ collapseEmpty: 'before',
+ sectionId: '{$section_id}',
+ location: {$loc_id},
+ width: {$width},
+ height: {$height}
+ });
+ });
+ </script>
+ </div>
+ </div>
+HTML;
+ }
+
+ /**
+ * Returns the complete ad div with snippet to be inserted into the page
+ *
+ * @param string $spot top, side, inline, or belowpost
+ * @param string $snippet The snippet to insert into the div
+ * @param array $css_classes
+ * @return string The supporting ad unit div
+ *
+ * @since 7.1
+ */
+ function get_ad_div( $spot, $snippet, array $css_classes = array() ) {
+ if ( empty( $css_classes ) ) {
+ $css_classes = array();
+ }
+
+ $css_classes[] = 'wpcnt';
+ if ( 'top' == $spot ) {
+ $css_classes[] = 'wpcnt-header';
+ }
+
+ $spot = esc_attr( $spot );
+ $classes = esc_attr( implode( ' ', $css_classes ) );
+ $about = esc_html__( 'Advertisements', 'jetpack' );
+ return <<<HTML
+ <div class="$classes">
+ <div class="wpa">
+ <span class="wpa-about">$about</span>
+ <div class="u $spot">
+ $snippet
+ </div>
+ </div>
+ </div>
+HTML;
+ }
+
+ /**
+ * Check the reasons to bail before we attempt to insert ads.
+ *
+ * @return true if we should bail (don't insert ads)
+ *
+ * @since 4.5.0
+ */
+ public function should_bail() {
+ return ! $this->option( 'wordads_approved' ) || (bool) $this->option( 'wordads_unsafe' );
+ }
+
+ /**
+ * Returns markup for HTML5 house ad base on unit
+ *
+ * @param string $unit mrec, widesky, or leaderboard
+ * @return string markup for HTML5 house ad
+ *
+ * @since 4.7.0
+ */
+ public function get_house_ad( $unit = 'mrec' ) {
+
+ switch ( $unit ) {
+ case 'widesky':
+ $width = 160;
+ $height = 600;
+ break;
+ case 'leaderboard':
+ $width = 728;
+ $height = 90;
+ break;
+ case 'mrec':
+ default:
+ $width = 300;
+ $height = 250;
+ break;
+ }
+
+ return <<<HTML
+ <iframe
+ src="https://s0.wp.com/wp-content/blog-plugins/wordads/house/html5/$unit/index.html"
+ width="$width"
+ height="$height"
+ frameborder="0"
+ scrolling="no"
+ marginheight="0"
+ marginwidth="0">
+ </iframe>
+HTML;
+ }
+
+ /**
+ * Activation hook actions
+ *
+ * @since 4.5.0
+ */
+ public static function activate() {
+ WordAds_API::update_wordads_status_from_api();
+ }
+}
+
+add_action( 'jetpack_activate_module_wordads', array( 'WordAds', 'activate' ) );
+add_action( 'jetpack_activate_module_wordads', array( 'WordAds_Cron', 'activate' ) );
+add_action( 'jetpack_deactivate_module_wordads', array( 'WordAds_Cron', 'deactivate' ) );
+
+global $wordads;
+$wordads = new WordAds();
diff --git a/plugins/jetpack/modules/wpcc.php b/plugins/jetpack/modules/wpcc.php
new file mode 100644
index 00000000..88c13090
--- /dev/null
+++ b/plugins/jetpack/modules/wpcc.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Deprecated. No longer needed.
+ *
+ * @package Jetpack
+ */
diff --git a/plugins/jetpack/modules/wpcom-block-editor/class-jetpack-wpcom-block-editor.php b/plugins/jetpack/modules/wpcom-block-editor/class-jetpack-wpcom-block-editor.php
new file mode 100644
index 00000000..7fe46fc4
--- /dev/null
+++ b/plugins/jetpack/modules/wpcom-block-editor/class-jetpack-wpcom-block-editor.php
@@ -0,0 +1,337 @@
+<?php
+/**
+ * WordPress.com Block Editor
+ * Allow new block editor posts to be composed on WordPress.com.
+ * This is auto-loaded as of Jetpack v7.4 for sites connected to WordPress.com only.
+ *
+ * @package Jetpack
+ */
+
+/**
+ * WordPress.com Block editor for Jetpack
+ */
+class Jetpack_WPCOM_Block_Editor {
+ /**
+ * ID of the user who signed the nonce.
+ *
+ * @var int
+ */
+ private $nonce_user_id;
+
+ /**
+ * Singleton
+ */
+ public static function init() {
+ static $instance = false;
+
+ if ( ! $instance ) {
+ $instance = new Jetpack_WPCOM_Block_Editor();
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Jetpack_WPCOM_Block_Editor constructor.
+ */
+ private function __construct() {
+ if ( $this->is_iframed_block_editor() ) {
+ add_action( 'admin_init', array( $this, 'disable_send_frame_options_header' ), 9 );
+ add_filter( 'admin_body_class', array( $this, 'add_iframed_body_class' ) );
+ }
+
+ add_action( 'login_init', array( $this, 'allow_block_editor_login' ), 1 );
+ add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_scripts' ) );
+ add_filter( 'mce_external_plugins', array( $this, 'add_tinymce_plugins' ) );
+ }
+
+ /**
+ * Checks if we are embedding the block editor in an iframe in WordPress.com.
+ *
+ * @return bool Whether the current request is from the iframed block editor.
+ */
+ public function is_iframed_block_editor() {
+ global $pagenow;
+
+ // phpcs:ignore WordPress.Security.NonceVerification
+ return ( 'post.php' === $pagenow || 'post-new.php' === $pagenow ) && ! empty( $_GET['frame-nonce'] );
+ }
+
+ /**
+ * Prevents frame options header from firing if this is a whitelisted iframe request.
+ */
+ public function disable_send_frame_options_header() {
+ // phpcs:ignore WordPress.Security.NonceVerification
+ if ( $this->framing_allowed( $_GET['frame-nonce'] ) ) {
+ remove_action( 'admin_init', 'send_frame_options_header' );
+ }
+ }
+
+ /**
+ * Adds custom admin body class if this is a whitelisted iframe request.
+ *
+ * @param string $classes Admin body classes.
+ * @return string
+ */
+ public function add_iframed_body_class( $classes ) {
+ // phpcs:ignore WordPress.Security.NonceVerification
+ if ( $this->framing_allowed( $_GET['frame-nonce'] ) ) {
+ $classes .= ' is-iframed ';
+ }
+
+ return $classes;
+ }
+
+ /**
+ * Allows to iframe the login page if a user is logged out
+ * while trying to access the block editor from wordpress.com.
+ */
+ public function allow_block_editor_login() {
+ // phpcs:ignore WordPress.Security.NonceVerification
+ if ( empty( $_REQUEST['redirect_to'] ) ) {
+ return;
+ }
+
+ // phpcs:ignore WordPress.Security.NonceVerification
+ $query = wp_parse_url( urldecode( $_REQUEST['redirect_to'] ), PHP_URL_QUERY );
+ $args = wp_parse_args( $query );
+
+ // Check nonce and make sure this is a Gutenframe request.
+ if ( ! empty( $args['frame-nonce'] ) && $this->framing_allowed( $args['frame-nonce'] ) ) {
+
+ // If SSO is active, we'll let WordPress.com handle authentication...
+ if ( Jetpack::is_module_active( 'sso' ) ) {
+ // ...but only if it's not an Atomic site. They already do that.
+ if ( ! jetpack_is_atomic_site() ) {
+ add_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_true' );
+ }
+ } else {
+ $_REQUEST['interim-login'] = true;
+ add_action( 'wp_login', array( $this, 'do_redirect' ) );
+ add_action( 'login_form', array( $this, 'add_login_html' ) );
+ add_filter( 'wp_login_errors', array( $this, 'add_login_message' ) );
+ remove_action( 'login_init', 'send_frame_options_header' );
+ wp_add_inline_style( 'login', '.interim-login #login{padding-top:8%}' );
+ }
+ }
+ }
+
+ /**
+ * Adds a login message.
+ *
+ * Intended to soften the expectation mismatch of ending up with a login screen rather than the editor.
+ *
+ * @param WP_Error $errors WP Error object.
+ * @return \WP_Error
+ */
+ public function add_login_message( $errors ) {
+ $errors->remove( 'expired' );
+ $errors->add( 'info', __( 'Before we continue, please log in to your Jetpack site.', 'jetpack' ), 'message' );
+
+ return $errors;
+ }
+
+ /**
+ * Maintains the `redirect_to` parameter in login form links.
+ * Adds visual feedback of login in progress.
+ */
+ public function add_login_html() {
+ ?>
+ <input type="hidden" name="redirect_to" value="<?php echo esc_url( $_REQUEST['redirect_to'] ); ?>" />
+ <script type="application/javascript">
+ document.getElementById( 'loginform' ).addEventListener( 'submit' , function() {
+ document.getElementById( 'wp-submit' ).setAttribute( 'disabled', 'disabled' );
+ document.getElementById( 'wp-submit' ).value = '<?php echo esc_js( __( 'Logging In...', 'jetpack' ) ); ?>';
+ } );
+ </script>
+ <?php
+ }
+
+ /**
+ * Does the redirect to the block editor.
+ */
+ public function do_redirect() {
+ wp_redirect( $GLOBALS['redirect_to'] );
+ exit;
+ }
+
+ /**
+ * Checks whether this is a whitelisted iframe request.
+ *
+ * @param string $nonce Nonce to verify.
+ * @return bool
+ */
+ public function framing_allowed( $nonce ) {
+ $verified = $this->verify_frame_nonce( $nonce, 'frame-' . Jetpack_Options::get_option( 'id' ) );
+
+ if ( is_wp_error( $verified ) ) {
+ wp_die( $verified ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ }
+
+ if ( $verified && ! defined( 'IFRAME_REQUEST' ) ) {
+ define( 'IFRAME_REQUEST', true );
+ }
+
+ return (bool) $verified;
+ }
+
+ /**
+ * Verify that correct nonce was used with time limit.
+ *
+ * The user is given an amount of time to use the token, so therefore, since the
+ * UID and $action remain the same, the independent variable is the time.
+ *
+ * @param string $nonce Nonce that was used in the form to verify.
+ * @param string $action Should give context to what is taking place and be the same when nonce was created.
+ * @return boolean|WP_Error Whether the nonce is valid.
+ */
+ public function verify_frame_nonce( $nonce, $action ) {
+ if ( empty( $nonce ) ) {
+ return false;
+ }
+
+ list( $expiration, $user_id, $hash ) = explode( ':', $nonce, 3 );
+
+ $this->nonce_user_id = (int) $user_id;
+ if ( ! $this->nonce_user_id ) {
+ return false;
+ }
+
+ $token = Jetpack_Data::get_access_token( $this->nonce_user_id );
+ if ( ! $token ) {
+ return false;
+ }
+
+ /*
+ * Failures must return `false` (blocking the iframe) prior to the
+ * signature verification.
+ */
+
+ add_filter( 'salt', array( $this, 'filter_salt' ), 10, 2 );
+ $expected_hash = wp_hash( "$expiration|$action|{$this->nonce_user_id}", 'jetpack_frame_nonce' );
+ remove_filter( 'salt', array( $this, 'filter_salt' ) );
+
+ if ( ! hash_equals( $hash, $expected_hash ) ) {
+ return false;
+ }
+
+ /*
+ * Failures may return `WP_Error` (showing an error in the iframe) after the
+ * signature verification passes.
+ */
+
+ if ( time() > $expiration ) {
+ return new WP_Error( 'nonce_invalid_expired', 'Expired nonce.', array( 'status' => 401 ) );
+ }
+
+ // Check if it matches the current user, unless they're trying to log in.
+ if ( get_current_user_id() !== $this->nonce_user_id && ! doing_action( 'login_init' ) ) {
+ return new WP_Error( 'nonce_invalid_user_mismatch', 'User ID mismatch.', array( 'status' => 401 ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Filters the WordPress salt.
+ *
+ * @param string $salt Salt for the given scheme.
+ * @param string $scheme Authentication scheme.
+ * @return string
+ */
+ public function filter_salt( $salt, $scheme ) {
+ if ( 'jetpack_frame_nonce' === $scheme ) {
+ $token = Jetpack_Data::get_access_token( $this->nonce_user_id );
+
+ if ( $token ) {
+ $salt = $token->secret;
+ }
+ }
+
+ return $salt;
+ }
+
+ /**
+ * Enqueue the scripts for the WordPress.com block editor integration.
+ */
+ public function enqueue_scripts() {
+ $debug = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
+ $version = gmdate( 'Ymd' );
+
+ $src_common = $debug
+ ? '//widgets.wp.com/wpcom-block-editor/common.js?minify=false'
+ : '//widgets.wp.com/wpcom-block-editor/common.min.js';
+
+ wp_enqueue_script(
+ 'wpcom-block-editor-common',
+ $src_common,
+ array( 'lodash', 'wp-compose', 'wp-data', 'wp-editor', 'wp-rich-text' ),
+ $version,
+ true
+ );
+ wp_localize_script(
+ 'wpcom-block-editor-common',
+ 'wpcomGutenberg',
+ array(
+ 'switchToClassic' => array(
+ 'isVisible' => $this->is_iframed_block_editor(),
+ 'label' => __( 'Switch to Classic Editor', 'jetpack' ),
+ 'url' => Jetpack_Calypsoify::getInstance()->get_switch_to_classic_editor_url(),
+ ),
+ 'richTextToolbar' => array(
+ 'justify' => __( 'Justify', 'jetpack' ),
+ 'underline' => __( 'Underline', 'jetpack' ),
+ ),
+ )
+ );
+
+ $src_styles = $debug
+ ? '//widgets.wp.com/wpcom-block-editor/common.css?minify=false'
+ : '//widgets.wp.com/wpcom-block-editor/common.min.css';
+ wp_enqueue_style(
+ 'wpcom-block-editor-styles',
+ $src_styles,
+ array(),
+ $version
+ );
+
+ if ( $this->is_iframed_block_editor() ) {
+ $src_calypso_iframe_bridge = $debug
+ ? '//widgets.wp.com/wpcom-block-editor/calypso-iframe-bridge-server.js?minify=false'
+ : '//widgets.wp.com/wpcom-block-editor/calypso-iframe-bridge-server.min.js';
+
+ wp_enqueue_script(
+ 'wpcom-block-editor-calypso-iframe-bridge',
+ $src_calypso_iframe_bridge,
+ array( 'calypsoify_wpadminmods_js', 'jquery', 'lodash', 'react', 'wp-blocks', 'wp-data', 'wp-hooks', 'wp-tinymce', 'wp-url' ),
+ $version,
+ true
+ );
+ }
+ }
+
+ /**
+ * Register the Tiny MCE plugins for the WordPress.com block editor integration.
+ *
+ * @param array $plugin_array An array of external Tiny MCE plugins.
+ * @return array External TinyMCE plugins.
+ */
+ public function add_tinymce_plugins( $plugin_array ) {
+ if ( $this->is_iframed_block_editor() ) {
+ $debug = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
+ $src_calypso_tinymce = $debug
+ ? '//widgets.wp.com/wpcom-block-editor/calypso-tinymce.js?minify=false'
+ : '//widgets.wp.com/wpcom-block-editor/calypso-tinymce.min.js';
+
+ $plugin_array['gutenberg-wpcom-iframe-media-modal'] = add_query_arg(
+ 'v',
+ gmdate( 'YW' ),
+ $src_calypso_tinymce
+ );
+ }
+
+ return $plugin_array;
+ }
+}
+
+Jetpack_WPCOM_Block_Editor::init();
diff --git a/plugins/jetpack/modules/wpgroho.js b/plugins/jetpack/modules/wpgroho.js
new file mode 100644
index 00000000..631fdb5b
--- /dev/null
+++ b/plugins/jetpack/modules/wpgroho.js
@@ -0,0 +1,43 @@
+/* global WPGroHo:true, Gravatar */
+WPGroHo = jQuery.extend(
+ {
+ my_hash: '',
+ data: {},
+ renderers: {},
+ syncProfileData: function( hash, id ) {
+ if ( ! WPGroHo.data[ hash ] ) {
+ WPGroHo.data[ hash ] = {};
+ jQuery( 'div.grofile-hash-map-' + hash + ' span' ).each( function() {
+ WPGroHo.data[ hash ][ this.className ] = jQuery( this ).text();
+ } );
+ }
+
+ WPGroHo.appendProfileData( WPGroHo.data[ hash ], hash, id );
+ },
+ appendProfileData: function( data, hash, id ) {
+ for ( var key in data ) {
+ if ( jQuery.isFunction( WPGroHo.renderers[ key ] ) ) {
+ return WPGroHo.renderers[ key ]( data[ key ], hash, id, key );
+ }
+
+ jQuery( '#' + id )
+ .find( 'h4' )
+ .after( jQuery( '<p class="grav-extra ' + key + '" />' ).html( data[ key ] ) );
+ }
+ },
+ },
+ WPGroHo
+);
+
+jQuery( document ).ready( function() {
+ if ( 'undefined' === typeof Gravatar ) {
+ return;
+ }
+
+ Gravatar.profile_cb = function( h, d ) {
+ WPGroHo.syncProfileData( h, d );
+ };
+
+ Gravatar.my_hash = WPGroHo.my_hash;
+ Gravatar.init( 'body', '#wpadminbar' );
+} );