Файловый менеджер - Редактировать - /var/www/xthruster/html/wp-content/uploads/flags/related-posts.tar
Назад
related-posts.js 0000644 00000024341 14722073315 0007700 0 ustar 00 /* globals related_posts_js_options */ /** * Load related posts */ ( function () { 'use strict'; 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'; var relatedPosts = document.querySelector( '#jp-relatedposts' ); if ( ! relatedPosts ) { return false; } if ( relatedPosts.hasAttribute( 'data-exclude' ) ) { args += '&relatedposts_exclude=' + relatedPosts.getAttribute( '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 anchorTitle = post.title; var anchor = document.createElement( 'a' ); anchor.setAttribute( 'class', classNames ); anchor.setAttribute( 'href', post.url ); anchor.setAttribute( 'title', anchorTitle ); anchor.setAttribute( 'data-origin', post.url_meta.origin ); anchor.setAttribute( 'data-position', post.url_meta.position ); if ( '' !== post.rel ) { anchor.setAttribute( 'rel', post.rel ); } var div = document.createElement( 'div' ); div.appendChild( anchor ); var anchorHTML = div.innerHTML; return [ anchorHTML.substring( 0, anchorHTML.length - 4 ), '</a>' ]; }, generateMinimalHtml: function ( posts, options ) { var self = this; var html = ''; posts.forEach( function ( post, index ) { 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 += '<time class="jp-relatedposts-post-date" datetime="' + post.date + '">' + post.date + '</time>'; } 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 = ''; posts.forEach( function ( post, index ) { 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'; } var dummyContainer = document.createElement( 'p' ); dummyContainer.innerHTML = post.excerpt; var excerpt = dummyContainer.textContent; 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" loading="lazy" src="' + post.img.src + '" width="' + post.img.width + '" height="' + post.img.height + ( post.img.srcset ? '" srcset="' + post.img.srcset : '' ) + ( post.img.sizes ? '" sizes="' + post.img.sizes : '' ) + '" alt="' + post.img.alt_text + '" />' + 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">' + excerpt + '</p>'; if ( options.showDate ) { html += '<time class="jp-relatedposts-post-date" datetime="' + post.date + '">' + post.date + '</time>'; } 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 = document.querySelectorAll( '#jp-relatedposts .jp-relatedposts-post-nothumbs .jp-relatedposts-post-excerpt' ); if ( ! elements.length ) { return; } var firstElementStyles = getComputedStyle( elements[ 0 ] ); var fontSize = parseInt( firstElementStyles.fontSize, 10 ); var lineHeight = parseInt( firstElementStyles.lineHeight, 10 ); // Show 5 lines of text for ( var i = 0; i < elements.length; i++ ) { elements[ i ].style.maxHeight = ( 5 * lineHeight ) / fontSize + 'em'; } }, getTrackedUrl: function ( anchor ) { var args = 'relatedposts_hit=1'; args += '&relatedposts_origin=' + anchor.getAttribute( 'data-origin' ); args += '&relatedposts_position=' + anchor.getAttribute( '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(); var posts = document.querySelectorAll( '#jp-relatedposts a.jp-relatedposts-post-a' ); Array.prototype.forEach.call( posts, function ( post ) { document.addEventListener( 'click', function () { post.href = jprp.getTrackedUrl( post ); } ); } ); } /** * Initialize Related Posts. */ function startRelatedPosts() { jprp.cleanupTrackedUrl(); var endpointURL = jprp.getEndpointURL(); var relatedPosts = document.querySelector( '#jp-relatedposts' ); if ( ! endpointURL ) { return; } if ( document.querySelectorAll( '#jp-relatedposts .jp-relatedposts-post' ).length ) { afterPostsHaveLoaded(); return; } var request = new XMLHttpRequest(); request.open( 'GET', endpointURL, true ); request.setRequestHeader( 'x-requested-with', 'XMLHttpRequest' ); request.onreadystatechange = function () { if ( this.readyState === XMLHttpRequest.DONE && this.status === 200 ) { try { var response = JSON.parse( request.responseText ); 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 ); var div = document.createElement( 'div' ); relatedPosts.appendChild( div ); div.outerHTML = html; if ( options.showDate ) { var dates = relatedPosts.querySelectorAll( '.jp-relatedposts-post-date' ); Array.prototype.forEach.call( dates, function ( date ) { date.style.display = 'block'; } ); } relatedPosts.style.display = 'block'; afterPostsHaveLoaded(); } catch ( error ) { // Do nothing } } }; request.send(); } function init() { 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(); } } if ( document.readyState !== 'loading' ) { init(); } else { document.addEventListener( 'DOMContentLoaded', init ); } } )(); rtl/related-posts-rtl.css 0000644 00000010725 14722073315 0011455 0 ustar 00 /* 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; } } related-posts-customizer.js 0000644 00000001336 14722073315 0012101 0 ustar 00 /** * 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 ); readme.md 0000644 00000002603 14722073315 0006330 0 ustar 00 # Related Posts The basic code flow for Related Posts (legacy version) is: 1. Generate an empty DIV to be added to the page. 2. Make an API call to get the working post IDs. 3. Call `get_related_post_data_for_post()` for each ID. 4. Use generated data to update the DIV once page load is complete. For the block-based version: 1. Make an API call to get the working post IDs. 2. Call `get_related_post_data_for_post()` for each ID. 3. Use generated data to render the block when called. ## Prerequisites If using a block theme, the site-wide Related Posts setting will be ignored. For other themes, the site-wide setting is honored as long as a Related Posts block or shortcode is not found on the page. The Related Posts code paths will only output once per page. In both cases, the feature needs to be turned on in the Jetpack settings. This makes sense for the legacy widget but can be a bit confusing for the block. It will not be visible in the block "picker" until enabled. ## API Usage The block depends on wpcom for the working post IDs. See `get_related_post_ids()` for details. The returned data should look like this: ``` [{"id":919},{"id":9},{"id":903}] ``` Note the data used in rendering is all generated locally once the post IDs have been provided. ## Block Source The source for the Gutenberg block lives separately at: `projects/plugins/jetpack/extensions/blocks/related-posts/` related-posts.css 0000644 00000016624 14722073315 0010061 0 ustar 00 /** * Jetpack related posts */ /** * The Gutenberg block */ .jp-related-posts-i2 { margin-top: 1.5rem; } .jp-related-posts-i2__list { --hgap: 1rem; display: flex; flex-wrap: wrap; column-gap: var(--hgap); row-gap: 2rem; margin: 0; padding: 0; list-style-type: none; } .jp-related-posts-i2__post { display: flex; flex-direction: column; /* Default: 2 items by row */ flex-basis: calc( ( 100% - var(--hgap) ) / 2 ); } /* Quantity qeuries: see https://alistapart.com/article/quantity-queries-for-css/ */ .jp-related-posts-i2__post:nth-last-child(n+3):first-child, .jp-related-posts-i2__post:nth-last-child(n+3):first-child ~ * { /* From 3 total items on, 3 items by row */ flex-basis: calc( ( 100% - var(--hgap) * 2 ) / 3 ); } .jp-related-posts-i2__post:nth-last-child(4):first-child, .jp-related-posts-i2__post:nth-last-child(4):first-child ~ * { /* Exception for 4 total items: 2 items by row */ flex-basis: calc( ( 100% - var(--hgap) ) / 2 ); } .jp-related-posts-i2__post-link { display: flex; flex-direction: column; row-gap: 0.5rem; width: 100%; margin-bottom: 1rem; line-height: 1.2; } .jp-related-posts-i2__post-link:focus-visible { outline-offset: 2px; } .jp-related-posts-i2__post-img { order: -1; max-width: 100%; } .jp-related-posts-i2__post-defs { margin: 0; list-style-type: unset; } /* Hide, except from screen readers */ .jp-related-posts-i2__post-defs dt { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(1px, 1px, 1px, 1px); white-space: nowrap; } .jp-related-posts-i2__post-defs dd { margin: 0; } /* List view */ .jp-relatedposts-i2[data-layout="list"] .jp-related-posts-i2__list { display: block; } .jp-relatedposts-i2[data-layout="list"] .jp-related-posts-i2__post { margin-bottom: 2rem; } /* Breakpoints */ @media only screen and (max-width: 640px) { .jp-related-posts-i2__list { display: block; } .jp-related-posts-i2__post { margin-bottom: 2rem; } } /* 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 #dcdcde; 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, #jp-relatedposts .jp-relatedposts-items time { 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, #jp-relatedposts .jp-relatedposts-items time { 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 { height: auto; 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; } } /* * Hide the related post section in the print view of a post */ @media print { .jp-relatedposts { display:none !important; } } related-posts-rtl.css 0000644 00000013345 14722073315 0010655 0 ustar 00 .jp-related-posts-i2{margin-top:1.5rem}.jp-related-posts-i2__list{--hgap:1rem;column-gap:var(--hgap);display:flex;flex-wrap:wrap;list-style-type:none;margin:0;padding:0;row-gap:2rem}.jp-related-posts-i2__post{display:flex;flex-basis:calc((100% - var(--hgap))/2);flex-direction:column}.jp-related-posts-i2__post:nth-last-child(n+3):first-child,.jp-related-posts-i2__post:nth-last-child(n+3):first-child~*{flex-basis:calc(33.33333% - var(--hgap)*2/3)}.jp-related-posts-i2__post:nth-last-child(4):first-child,.jp-related-posts-i2__post:nth-last-child(4):first-child~*{flex-basis:calc((100% - var(--hgap))/2)}.jp-related-posts-i2__post-link{display:flex;flex-direction:column;line-height:1.2;margin-bottom:1rem;row-gap:.5rem;width:100%}.jp-related-posts-i2__post-link:focus-visible{outline-offset:2px}.jp-related-posts-i2__post-img{max-width:100%;order:-1}.jp-related-posts-i2__post-defs{list-style-type:unset;margin:0}.jp-related-posts-i2__post-defs dt{height:1px;overflow:hidden;position:absolute;width:1px;clip:rect(1px,1px,1px,1px);white-space:nowrap}.jp-related-posts-i2__post-defs dd{margin:0}.jp-relatedposts-i2[data-layout=list] .jp-related-posts-i2__list{display:block}.jp-relatedposts-i2[data-layout=list] .jp-related-posts-i2__post{margin-bottom:2rem}@media only screen and (max-width:640px){.jp-related-posts-i2__list{display:block}.jp-related-posts-i2__post{margin-bottom:2rem}}#jp-relatedposts{clear:both;display:none;margin:1em 0;padding-top:1em;position:relative}.jp-relatedposts:after{clear:both;content:"";display:block}#jp-relatedposts h3.jp-relatedposts-headline{display:inline-block;float:right;font-family:inherit;font-size:9pt;font-weight:700;margin:0 0 1em}#jp-relatedposts h3.jp-relatedposts-headline em:before{border-top:1px solid #0003;content:"";display:block;margin-bottom:1em;min-width:30px;width:100%}#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{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;float:right;margin:0 0 1em;width:33%}#jp-relatedposts .jp-relatedposts-items-visual .jp-relatedposts-post{filter:alpha(opacity=80);-moz-opacity:.8;opacity:.8;padding-left:20px}#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}#jp-relatedposts .jp-relatedposts-items p,#jp-relatedposts .jp-relatedposts-items time,#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{border-bottom:0;bottom:0;display:block;left:0;position:absolute;right:0;top:0}#jp-relatedposts .jp-relatedposts-items p,#jp-relatedposts .jp-relatedposts-items time{margin-bottom:0}#jp-relatedposts .jp-relatedposts-items-visual h4.jp-relatedposts-post-title{display:block;font-family:inherit;margin:0;max-width:100%;text-transform:none}#jp-relatedposts .jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-title a{filter:alpha(opacity=100);font-size:inherit;font-weight:400;-moz-opacity:1;opacity:1;text-decoration:none}#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{height:auto;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 .jp-relatedposts-items-visual div.jp-relatedposts-post-thumbs p.jp-relatedposts-post-excerpt,.jp-relatedposts-items .jp-relatedposts-post .jp-relatedposts-post-date{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;margin-left:3%;max-width:33%;overflow:hidden}#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{clear:both;margin:0 0 1em;width:100%}#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;margin-left:0;max-width:100%}}@media print{.jp-relatedposts{display:none!important}} jetpack-related-posts.php 0000644 00000211326 14722073315 0011473 0 ustar 00 <?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * The Jetpack_RelatedPosts class. * * @package automattic/jetpack */ use Automattic\Jetpack\Assets; use Automattic\Jetpack\Blocks; use Automattic\Jetpack\Sync\Settings; /** * The Jetpack_RelatedPosts class. */ class Jetpack_RelatedPosts { const VERSION = '20240116'; const SHORTCODE = 'jetpack-related-posts'; /** * Instance of the class. * * @var Jetpack_RelatedPosts */ private static $instance = null; /** * Instance of the raw class (?). * * @var Jetpack_RelatedPosts */ 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; } /** * Options. * * @var array $options */ protected $options; /** * Allow feature toggle variable. * * @var bool */ protected $allow_feature_toggle; /** * Blog character set. * * @var mixed */ protected $blog_charset; /** * Convert character set. * * @var bool */ protected $convert_charset; /** * Previous Post ID * * @var int */ protected $previous_post_id; /** * Shortcode usage. * * @var bool */ protected $found_shortcode = false; /** * Constructor for Jetpack_RelatedPosts. * * @uses get_option, add_action, apply_filters */ 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' ) ) { require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media-summary.php'; } // Add Related Posts to the REST API Post response. add_action( 'rest_api_init', array( $this, 'rest_register_related_posts' ) ); } /** * Get the blog ID. * * @return Object current blog id. */ 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 */ 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 an eligible 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 */ 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_client_rendered_html_unsupported' ) ); if ( ! $this->enabled_for_request() ) { return; } if ( isset( $_GET['relatedposts'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading and checking if we need to generate a list of excuded posts, does not update anything on the site. $excludes = $this->parse_numeric_get_arg( 'relatedposts_exclude' ); $this->action_frontend_init_ajax( $excludes ); } else { if ( isset( $_GET['relatedposts_hit'] ) && isset( $_GET['relatedposts_origin'] ) && isset( $_GET['relatedposts_position'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- checking if fields are set to setup tracking, nothing is changing on the site. $this->previous_post_id = (int) $_GET['relatedposts_origin']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- fetching a previous post ID for tracking, nothing is changing on the site. $this->log_click( $this->previous_post_id, get_the_ID(), sanitize_text_field( wp_unslash( $_GET['relatedposts_position'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- logging the click for tracking, nothing is changing on the site. } $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, if the 'get_the_excerpt' * hook is in the current filter list, or if the site is running an FSE/Site Editor theme. * * @filter the_content * * @param string $content Post content. * * @return string */ public function filter_add_target_to_dom( $content ) { // Do not output related posts for ActivityPub requests. if ( function_exists( '\Activitypub\is_activitypub_request' ) && \Activitypub\is_activitypub_request() ) { return $content; } if ( has_block( 'jetpack/related-posts' ) || Blocks::is_fse_theme() ) { return $content; } if ( ! $this->found_shortcode && ! doing_filter( 'get_the_excerpt' ) ) { if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) { $content .= "\n" . $this->get_server_rendered_html(); } else { $content .= "\n" . $this->get_client_rendered_html(); } } return $content; } /** * Render static markup based on the Gutenberg block code * * @return string Rendered related posts HTML. */ public function get_server_rendered_html() { $rp_settings = $this->get_options(); $block_rp_settings = array( 'displayThumbnails' => $rp_settings['show_thumbnails'], 'showHeadline' => $rp_settings['show_headline'], 'displayDate' => isset( $rp_settings['show_date'] ) ? (bool) $rp_settings['show_date'] : true, 'displayContext' => isset( $rp_settings['show_context'] ) && $rp_settings['show_context'], 'postLayout' => isset( $rp_settings['layout'] ) ? $rp_settings['layout'] : 'grid', 'postsToShow' => isset( $rp_settings['size'] ) ? $rp_settings['size'] : 3, /** This filter is already documented in modules/related-posts/jetpack-related-posts.php */ 'headline' => apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() ), 'isServerRendered' => true, ); return $this->render_block( $block_rp_settings, '' ); } /** * Looks for our shortcode on the unfiltered content, this has to execute early. * * @filter the_content * @param string $content - content of the post. * @uses has_shortcode * @return string $content */ 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 * @return string */ public function get_client_rendered_html() { if ( 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. * * @return string */ public function get_client_rendered_html_unsupported() { if ( 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 object. * @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'; $title = $related_post['title']; $url = $related_post['url']; $rel = $related_post['rel']; $img = ''; $list = ''; $item_markup = sprintf( '<li id="%1$s" class="jp-related-posts-i2__post">', esc_attr( $instance_id ) ); // Thumbnail if ( ! empty( $block_attributes['show_thumbnails'] ) && ! empty( $related_post['img']['src'] ) ) { $img = sprintf( '<img loading="lazy" class="jp-related-posts-i2__post-img" src="%1$s" alt="%2$s" %3$s/>', esc_url( $related_post['img']['src'] ), esc_attr( $related_post['img']['alt_text'] ), ( ! empty( $related_post['img']['srcset'] ) ? 'srcset="' . esc_attr( $related_post['img']['srcset'] ) . '"' : '' ) ); } // Link $item_markup .= sprintf( '<a id="%1$s" href="%2$s" class="jp-related-posts-i2__post-link" %3$s>%4$s%5$s</a>', esc_attr( $label_id ), esc_url( $url ), ( ! empty( $rel ) ? 'rel="' . esc_attr( $rel ) . '"' : '' ), esc_html( $title ), $img ); // Date if ( $block_attributes['show_date'] ) { $list .= '<dt>' . __( 'Date', 'jetpack' ) . '</dt>'; $list .= '<dd class="jp-related-posts-i2__post-date">'; $list .= esc_html( $related_post['date'] ); $list .= '</dd>'; } // Author if ( $block_attributes['show_author'] ) { $list .= '<dt>' . __( 'Author', 'jetpack' ) . '</dt>'; $list .= '<dd class="jp-related-posts-i2__post-author">'; $list .= esc_html( $related_post['author'] ); $list .= '</dd>'; } // Context if ( ( $block_attributes['show_context'] ) && ! empty( $related_post['block_context'] ) ) { // translators: this is followed by the reason why the item is related to the current post $list .= '<dt>' . __( 'In relation to', 'jetpack' ) . '</dt>'; $list .= '<dd class="jp-related-posts-i2__post-context">'; // Note: The original 'context' value is not used when rendering the block. // It is still generated and available for the legacy rendering code path though. // See './related-posts.js' for that usage. $block_context = $related_post['block_context']; if ( ! empty( $block_context['link'] ) ) { $list .= sprintf( '<a href="%1$s">%2$s</a>', esc_url( $block_context['link'] ), esc_html( $block_context['text'] ) ); } else { $list .= esc_html( $block_context['text'] ); } $list .= '</dd>'; } // Metadata if ( ! empty( $list ) ) { $item_markup .= '<dl class="jp-related-posts-i2__post-defs">' . $list . '</dl>'; } $item_markup .= '</li>'; return $item_markup; } /** * Render the list of related posts. * * @param array $posts The posts to render into the list. * @param array $block_attributes Block attributes. * @return string */ public function render_post_list( $posts, $block_attributes ) { $markup = ''; foreach ( $posts as $post ) { $markup .= $this->render_block_item( $post, $block_attributes ); } return sprintf( // role="list" is required for accessibility as VoiceOver ignores unstyled lists. '<ul class="jp-related-posts-i2__list" role="list" data-post-count="%1$s">%2$s</ul>', count( $posts ), $markup ); } /** * Render the related posts markup. * * @param array $attributes Block attributes. * @param string $content String containing the related Posts block content. * @param WP_Block $block The block object. * @return string */ public function render_block( $attributes, $content, $block = null ) { if ( ! jetpack_is_frontend() ) { return $content; } $wrapper_attributes = array(); $post_id = get_the_ID(); $block_attributes = array( 'headline' => isset( $attributes['headline'] ) ? $attributes['headline'] : null, 'show_thumbnails' => isset( $attributes['displayThumbnails'] ) && $attributes['displayThumbnails'], 'show_author' => isset( $attributes['displayAuthor'] ) ? (bool) $attributes['displayAuthor'] : false, 'show_headline' => isset( $attributes['displayHeadline'] ) ? (bool) $attributes['displayHeadline'] : false, '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( $post_id, array( 'size' => $block_attributes['size'], 'exclude_post_ids' => $excludes, ) ); if ( empty( $related_posts ) ) { return ''; } $list_markup = $this->render_post_list( $related_posts, $block_attributes ); if ( empty( $attributes['isServerRendered'] ) ) { // The get_server_rendered_html() path won't register a block, // so only apply block supports when not server rendered. $wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports(); } $headline_markup = ''; if ( isset( $block ) ) { foreach ( $block->inner_blocks as $inner_block ) { if ( 'core/heading' === $inner_block->name && ! empty( wp_strip_all_tags( $inner_block->inner_html ) ) ) { $headline_markup = trim( $inner_block->inner_html ); break; } } } if ( empty( $headline_markup ) && $block_attributes['show_headline'] === true ) { $headline = $block_attributes['headline']; if ( strlen( trim( $headline ) ) !== 0 ) { $headline_markup = sprintf( '<h3 class="jp-relatedposts-headline">%1$s</h3>', esc_html( $headline ) ); } } $display_markup = sprintf( '<nav class="jp-relatedposts-i2%1$s"%2$s data-layout="%3$s" aria-label="%6$s">%4$s%5$s</nav>', ! empty( $wrapper_attributes['class'] ) ? ' ' . esc_attr( $wrapper_attributes['class'] ) : '', ! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : '', esc_attr( $block_attributes['layout'] ), $headline_markup, $list_markup, empty( $headline_markup ) ? esc_attr__( 'Related Posts', 'jetpack' ) : esc_attr( wp_strip_all_tags( $headline_markup ) ) ); /** * Filter the output HTML of Related Posts. * * @module related-posts * * @since 10.7 * * @param string $display_markup HTML output of Related Posts. * @param int|false get_the_ID() Post ID of the post for which we are retrieving Related Posts. * @param array $related_posts Array of related posts. * @param array $block_attributes Array of Block attributes. */ return (string) apply_filters( 'jetpack_related_posts_display_markup', $display_markup, $post_id, $related_posts, $block_attributes ); } /** * ======================== * 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 ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- requests are used to generate a list of related posts we want to exclude. if ( is_string( $_GET[ $arg ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $result = explode( ',', sanitize_text_field( wp_unslash( $_GET[ $arg ] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } elseif ( is_array( $_GET[ $arg ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $args = array_map( 'sanitize_text_field', wp_unslash( $_GET[ $arg ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $result = array_values( $args ); } $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; } /** * Gets options. * * @param string $option_name - option we want to get. */ 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 - input we're parsing. * @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'] ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- expecting string, but may return bools. $current['enabled'] = true; $current['show_headline'] = ( isset( $input['show_headline'] ) && '1' == $input['show_headline'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $current['show_date'] = ( isset( $input['show_date'] ) && '1' == $input['show_date'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $current['show_context'] = ( isset( $input['show_context'] ) && '1' == $input['show_context'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $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__ */ 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, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $ui_settings // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- data is escaped when variable is set. ); } 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, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 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 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- data is escaped when variable is set. ); } } /** * Head JS/CSS for admin settings page. * * @uses esc_html__ * @return null */ public function print_setting_head() { // only dislay the Related Posts JavaScript on the Reading Settings Admin Page. $current_screen = get_current_screen(); if ( $current_screen === null ) { 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 = ''; } // phpcs:disable WordPress.Security.EscapeOutput.HeredocOutputNotEscaped -- Escaped above where needed. 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; // phpcs:enable WordPress.Security.EscapeOutput.HeredocOutputNotEscaped } /** * 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 int $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 - the post ID. * @param array $args - the arguments. * @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, true ) ) { $sanitized_post_types[] = $pt; } } if ( ! empty( $sanitized_post_types ) ) { $filters[] = array( 'terms' => array( 'post_type' => $sanitized_post_types ) ); } } elseif ( in_array( $args['post_type'], $valid_post_types, true ) && '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. * * @todo Rewrite this function with proper date handling rather than `strtotime()` and `date()`. * * @param array $date_range - the 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 ) ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date // Move back 1 period. return array( 'from' => date( 'Y-m-01', strtotime( '-1 month', $date_range['from'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date 'to' => date( 'Y-m-01', $date_range['to'] ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date ); } else { // Use current period. return array( 'from' => date( 'Y-m-01', $date_range['from'] ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date 'to' => date( 'Y-m-01', strtotime( '+1 month', $date_range['to'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date ); } } /** * 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 never */ 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'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- adds dummy content if we're in the customizer. // 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. $count_post_with_thumbnails = is_countable( $with_post_thumbnails ) ? count( $with_post_thumbnails ) : 0; $more = $options['size'] - $count_post_with_thumbnails; if ( 0 < $more ) { $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, $real_post->ID ) ); $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 wp_json_encode( $response ); exit(); } /** * Returns a UTF-8 encoded array of post information for the given post_id * * @param int $post_id - the post ID. * @param int $position - position of the post. * @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, $post->ID ) ), 'author' => $this->generate_related_post_display_author( $post->ID ), '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 * @since 7.9.0 - Change Default value to empty. * * @param string $link_rel Link rel attribute for Related Posts' link. Default is empty. * @param int $post->ID Post ID. */ 'rel' => apply_filters( 'jetpack_relatedposts_filter_post_link_rel', '', $post->ID ), /** * Filter the context displayed below each Related Post. * * This context is used when rendering the legacy 'widget' version of Related Posts. * It is not used when rendering the block-based version. See 'block_context' below for that. * * @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 int $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 ), // The context used when rendering as a Block. No filtering applied. 'block_context' => $this->generate_related_post_context_block( $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. * * @uses strip_shortcodes, wp_trim_words, __, apply_filters * * @param string $post_title Post title. * @param string $post_content Post content. * @param int $post_id Post ID. * * @return string */ protected function get_title( $post_title, $post_content, $post_id ) { if ( ! empty( $post_title ) ) { return wp_strip_all_tags( /** This filter is documented in core/src/wp-includes/post-template.php */ apply_filters( 'the_title', $post_title, $post_id ) ); } $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 - the post excerpt. * @param string $post_content - the 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 - the 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 ) { $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'] = ''; } $thumbnail_width = 0; $thumbnail_height = 0; if ( ! empty( $thumbnail_size['width'] ) ) { $thumbnail_width = $thumbnail_size['width']; $image_params['width'] = $thumbnail_width; } if ( ! empty( $thumbnail_size['height'] ) ) { $thumbnail_height = $thumbnail_size['height']; $image_params['height'] = $thumbnail_height; } $image_params['src'] = Jetpack_PostImages::fit_image_url( $img_url, $thumbnail_width, $thumbnail_height ); // Add a srcset to handle zoomed views and high-density screens. $srcset = Jetpack_PostImages::generate_cropped_srcset( $post_image, $thumbnail_width, $thumbnail_height ); if ( ! empty( $srcset ) ) { $image_params['srcset'] = $srcset; } } } return $image_params; } /** * Returns the string UTF-8 encoded * * @param string $text - the text we want to convert. * @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 - the ID of the post. * @param int $size - the size of the post. * @param array $filters - 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 - the post ID. * @param int $size - the size. * @param array $filters - some 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 ) { $transient_name = null; $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 ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- this is used for caching. // 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']; } } $user_agent = ''; if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) { $user_agent = strtolower( filter_var( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) ); } $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, $user_agent", 'sslverify' => true, 'body' => $body, ) ); // Oh no... return nothing don't cache errors. Also, don't cache HTTP 409 conflict responses. if ( is_wp_error( $response ) || WP_Http::CONFLICT === wp_remote_retrieve_response_code( $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 - the 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, true ) ) { $filtered[] = $hit; } } return $filtered; } /** * Generates the author byline for the related post. * * @param int $post_id - the post ID. * @uses get_post_field, get_the_author_meta * @return string */ protected function generate_related_post_display_author( $post_id ) { $post_author = get_post_field( 'post_author', $post_id ); $author_display_name = get_the_author_meta( 'display_name', $post_author ); if ( ! empty( $author_display_name ) ) { return $author_display_name; } return ''; } /** * 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 - the post ID. * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n * @return string */ protected function generate_related_post_context_block( $post_id ) { $categories = get_the_category( $post_id ); if ( is_array( $categories ) ) { foreach ( $categories as $category ) { $cat_link = get_category_link( $category ); if ( 'uncategorized' !== $category->slug && '' !== trim( $category->name ) ) { return array( 'text' => trim( $category->name ), 'link' => $cat_link, ); } } } $tags = get_the_terms( $post_id, 'post_tag' ); if ( is_array( $tags ) ) { foreach ( $tags as $tag ) { $tag_link = get_tag_link( $tag ); if ( '' !== trim( $tag->name ) ) { return array( 'text' => trim( $tag->name ), 'link' => $tag_link, ); } } } $comment_count = get_comments_number( $post_id ); if ( $comment_count > 0 ) { $comments_string = sprintf( // Translators: amount of comments. _n( 'With %s comment', 'With %s comments', $comment_count, 'jetpack' ), number_format_i18n( $comment_count ) ); $comments_link = get_comments_link( $post_id ); return array( 'text' => $comments_string, 'link' => $comments_link, ); } $fallback_string = __( 'Similar post', 'jetpack' ); return array( 'text' => $fallback_string, 'link' => '', ); } /** * 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 - the 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( // Translators: The category or tag name. 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( // Translators: the category or tag name. _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( // Translators: amount of comments. _n( 'With %s 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. * * @param int $post_id - the post ID. * @param int $to_post_id - the to post ID. * @param int $link_position - the link position. */ protected function log_click( $post_id, $to_post_id, $link_position ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable } /** * Determines if the current post is able to use related posts. * * @since 14.0 Checks for singular instead of single to allow usage on non-posts CPTs in block themes. * @uses self::get_options, is_admin, is_singular, apply_filters * @return bool */ protected function enabled_for_request() { /* * On block themes, allow usage on any singular view (post, page, CPT). * On classic themes, only allow usage on single posts by default. */ $enabled_on_singular_views = wp_is_block_theme() ? is_singular() : is_single(); $enabled = $enabled_on_singular_views && ! is_attachment() && ! is_admin() && ! is_embed() && ( ! $this->allow_feature_toggle() || $this->get_option( 'enabled' ) ); /** * Filter the Enabled value to allow related posts to be selectively enabled/disabled. * * @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. * * @uses self::enqueue_assets, self::setup_shortcode, add_filter */ 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 ); } /** * Determines if the scripts need be enqueued. * * @return bool */ protected function requires_scripts() { return ( ! ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) && ! has_block( 'jetpack/related-posts' ) && ! Blocks::is_fse_theme() ); } /** * Enqueues assets needed to do async loading of related posts. * * @param string $script - the script we're enqueing. * @param string $style - the style we're enqueing. * * @uses wp_enqueue_script, wp_enqueue_style, plugins_url */ protected function enqueue_assets( $script, $style ) { $dependencies = is_customize_preview() ? array( 'customize-base' ) : array(); // Do not enqueue scripts unless they are required. if ( $script && $this->requires_scripts() ) { wp_enqueue_script( 'jetpack_related-posts', Assets::get_file_url_for_environment( '_inc/build/related-posts/related-posts.min.js', 'modules/related-posts/related-posts.js' ), $dependencies, self::VERSION, false ); $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' ); add_action( 'amp_post_template_css', array( $this, 'render_amp_reader_mode_css' ) ); } } /** * Render AMP's reader mode CSS. */ public function render_amp_reader_mode_css() { echo file_get_contents( __DIR__ . '/related-posts.css' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- this is loading a CSS file. } /** * Sets up the shortcode processing. * * @uses add_filter, add_shortcode */ protected function setup_shortcode() { add_filter( 'the_content', array( $this, 'test_for_shortcode' ), 0 ); add_shortcode( self::SHORTCODE, array( $this, 'get_client_rendered_html' ) ); } /** * Return status of related posts toggle. */ 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 $allow_feature_toggle 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 */ public function rest_register_related_posts() { /** This filter is already documented in class.json-api-endpoints.php */ $post_types = apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) ); foreach ( $post_types as $post_type ) { register_rest_field( $post_type, '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. * * @uses self::get_for_post_id * * @return array */ public function rest_get_related_posts( $object ) { if ( ! isset( $object['id'] ) ) { return array(); } // If the Related Posts option is turned off, don't get the related posts. $options = \Jetpack_Options::get_option( 'relatedposts', array() ); if ( empty( $options['enabled'] ) || ! $options['enabled'] ) { return array(); } // If the current post don't contain a Related Posts block, don't get the related posts. if ( ! has_block( 'jetpack/related-posts' ) ) { return array(); } return $this->get_for_post_id( $object['id'], array( 'size' => 6 ) ); } } /** * 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 are done * programmatically. */ class Jetpack_RelatedPosts_Raw extends Jetpack_RelatedPosts { //phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace, Generic.Files.OneObjectStructurePerFile.MultipleFound /** * The query name we want to look up. * * @var string */ protected $query_name; /** * Allows callers of this class to tag each query with a unique name for tracking purposes. * * @param string $name - the name of the query. * @return Jetpack_RelatedPosts_Raw */ public function set_query_name( $name ) { $this->query_name = (string) $name; return $this; } /** * Initialize admin. */ public function action_admin_init() {} /** * Initialize front end. */ public function action_frontend_init() {} /** * Get options. */ public function get_options() { return array( 'enabled' => true, ); } /** * Workhorse method to return array of related posts ids matched by Elasticsearch. * * @param int $post_id - the post ID. * @param int $size - size of the post. * @param array $filters - filters we're using. * @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; } } class.related-posts-customize.php 0000644 00000022016 14722073315 0013174 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Assets; // 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 */ public $prefix = 'jetpack_relatedposts'; /** * Control to focus when customizer loads * * @var string */ public $focus = ''; /** * Class initialization. * * @since 4.4.0 */ public function __construct() { if ( ! wp_is_block_theme() ) { 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. */ public 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(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- content escaped in get_headline method } /** * 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 */ public 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 */ public function customize_controls_enqueue_scripts() { wp_enqueue_script( 'jetpack_related-posts-customizer', Assets::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, false ); } } // class end /** * Control that displays a message in Customizer. * * @since 4.4.0 * @todo break this out into its own file. */ class Jetpack_Message_Control extends WP_Customize_Control { // phpcs:ignore /** * 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();
| ver. 1.4 |
Github
|
.
| PHP 7.4.3-4ubuntu2.24 | Генерация страницы: 0.01 |
proxy
|
phpinfo
|
Настройка