Skip to content

Commit fb1ad35

Browse files
authored
Merge branch 'develop' into ari/remove-categories
2 parents d1530f6 + 345d837 commit fb1ad35

3 files changed

Lines changed: 291 additions & 24 deletions

File tree

assets/css/page-widgets/suggested-tasks.css

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,13 +240,16 @@
240240
border: 1px solid var(--prpl-color-border);
241241
}
242242

243-
.prpl-button-step {
243+
.prpl-button-step,
244+
.prpl-button {
244245
padding: 0.75rem 1.25rem;
246+
margin: 1rem 0;
245247
border: none;
246248
color: var(--prpl-color-text);
247249
font-weight: 600;
248250
border-radius: var(--prpl-border-radius);
249251
background-color: var(--prpl-background-banner);
252+
cursor: pointer;
250253

251254
/* WIP: pick exact color */
252255
transition: all 0.25s ease-in-out;
@@ -257,7 +260,7 @@
257260
display: block;
258261
width: 100%;
259262
height: 100%;
260-
background: var(--prpl-background-banner);
263+
background: var(--prpl-background-banner) !important;
261264

262265
/* WIP: pick exact color */
263266
position: absolute;
@@ -444,6 +447,7 @@
444447

445448
.prpl-button {
446449
cursor: pointer;
450+
margin: 0;
447451

448452
/* If the button has empty data-action attribute disable it. */
449453
&[data-action=""] {

assets/js/web-components/prpl-install-plugin.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* global customElements, HTMLElement, prplL10n, progressPlanner, progressPlannerAjaxRequest, prplSuggestedTask */
1+
/* global customElements, HTMLElement, prplL10n, progressPlannerAjaxRequest, prplSuggestedTask */
22
/*
33
* Install Plugin
44
*
@@ -81,12 +81,12 @@ customElements.define(
8181
`;
8282

8383
progressPlannerAjaxRequest( {
84-
url: progressPlanner.ajaxUrl,
84+
url: this.getAjaxUrl(),
8585
data: {
8686
action: 'progress_planner_install_plugin',
8787
plugin_slug: this.pluginSlug,
8888
plugin_name: this.pluginName,
89-
nonce: progressPlanner.nonce,
89+
nonce: this.getNonce(),
9090
},
9191
} )
9292
.then( () => thisObj.activatePlugin() )
@@ -102,12 +102,12 @@ customElements.define(
102102
`;
103103

104104
progressPlannerAjaxRequest( {
105-
url: progressPlanner.ajaxUrl,
105+
url: this.getAjaxUrl(),
106106
data: {
107107
action: 'progress_planner_activate_plugin',
108108
plugin_slug: thisObj.pluginSlug,
109109
plugin_name: thisObj.pluginName,
110-
nonce: progressPlanner.nonce,
110+
nonce: this.getNonce(),
111111
},
112112
} )
113113
.then( () => {
@@ -141,5 +141,25 @@ customElements.define(
141141
}
142142
} );
143143
}
144+
145+
/**
146+
* Get the AJAX URL.
147+
*
148+
* @return {string} The AJAX URL.
149+
*/
150+
getAjaxUrl() {
151+
return window.progressPlanner?.ajaxUrl ?? window.ajaxurl;
152+
}
153+
154+
/**
155+
* Get the nonce.
156+
*
157+
* @return {string} The nonce.
158+
*/
159+
getNonce() {
160+
return (
161+
window.progressPlanner?.nonce ?? window.prplSuggestedTask?.nonce
162+
);
163+
}
144164
}
145165
);

classes/admin/widgets/class-whats-new.php

Lines changed: 260 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,28 @@ public function get_blog_feed() {
4747

4848
// Transient expired, fetch new feed.
4949
if ( $feed_data['expires'] < \time() ) {
50-
// Get the feed using the REST API.
51-
$response = \wp_remote_get( \progress_planner()->get_ui__branding()->get_blog_feed_url() . '/wp-json/wp/v2/posts/?per_page=2' );
50+
// Get the feed URL - append /feed/ if not already present.
51+
$feed_url = \progress_planner()->get_ui__branding()->get_blog_feed_url();
52+
if ( ! \str_contains( $feed_url, '/feed' ) && ! \str_ends_with( $feed_url, '.rss' ) ) {
53+
$feed_url = \trailingslashit( $feed_url ) . 'feed/';
54+
}
55+
56+
// Fetch the RSS feed using WordPress's built-in function.
57+
$rss = \fetch_feed( $feed_url );
5258

53-
if ( 200 !== \wp_remote_retrieve_response_code( $response ) ) {
54-
// If we cant fetch the feed, we will try again later.
59+
if ( \is_wp_error( $rss ) ) {
60+
// If we can't fetch the feed, we will try again later.
5561
$feed_data['expires'] = \time() + 5 * MINUTE_IN_SECONDS;
5662
} else {
57-
$feed = \json_decode( \wp_remote_retrieve_body( $response ), true );
58-
59-
foreach ( $feed as $key => $post ) {
60-
// Get the featured media.
61-
$featured_media_id = $post['featured_media'];
62-
if ( $featured_media_id ) {
63-
$response = \wp_remote_get( \progress_planner()->get_remote_server_root_url() . '/wp-json/wp/v2/media/' . $featured_media_id );
64-
if ( ! \is_wp_error( $response ) ) {
65-
$media = \json_decode( \wp_remote_retrieve_body( $response ), true );
66-
67-
$post['featured_media'] = $media;
68-
}
63+
$feed = [];
64+
$max_items = $rss->get_item_quantity( 2 );
65+
$rss_items = $rss->get_items( 0, $max_items );
66+
67+
foreach ( $rss_items as $item ) {
68+
$post = $this->parse_rss_item( $item );
69+
if ( $post ) {
70+
$feed[] = $post;
6971
}
70-
$feed[ $key ] = $post;
7172
}
7273

7374
$feed_data['feed'] = $feed;
@@ -81,6 +82,248 @@ public function get_blog_feed() {
8182
return $feed_data['feed'];
8283
}
8384

85+
/**
86+
* Parse an RSS feed item and convert it to the expected format.
87+
*
88+
* @param \SimplePie\Item|null $item The RSS feed item.
89+
* @return array|null The parsed item data or null if parsing fails.
90+
*/
91+
private function parse_rss_item( $item ) {
92+
if ( ! $item ) {
93+
return null;
94+
}
95+
96+
$post = [
97+
'title' => [
98+
'rendered' => $item->get_title(),
99+
],
100+
'link' => $item->get_permalink(),
101+
'date' => $item->get_date( 'c' ),
102+
'excerpt' => [
103+
'rendered' => $item->get_description(),
104+
],
105+
'content' => [
106+
'rendered' => $item->get_content(),
107+
],
108+
'featured_media' => null,
109+
];
110+
111+
// Try to extract featured image from various sources.
112+
$featured_image_url = $this->extract_featured_image( $item );
113+
114+
if ( $featured_image_url ) {
115+
$post['featured_media'] = [
116+
'source_url' => $featured_image_url,
117+
'media_details' => [
118+
'sizes' => [
119+
'medium' => [
120+
'source_url' => $featured_image_url,
121+
],
122+
'medium_large' => [
123+
'source_url' => $featured_image_url,
124+
],
125+
],
126+
],
127+
];
128+
}
129+
130+
return $post;
131+
}
132+
133+
/**
134+
* Extract featured image URL from an RSS feed item.
135+
*
136+
* @param \SimplePie\Item $item The RSS feed item.
137+
* @return string|null The featured image URL or null if not found.
138+
*/
139+
private function extract_featured_image( $item ) {
140+
// Method 1: Try media:thumbnail namespace.
141+
$media_thumbnail = $item->get_item_tags( 'http://search.yahoo.com/mrss/', 'thumbnail' );
142+
if ( $media_thumbnail && isset( $media_thumbnail[0]['attribs']['']['url'] ) ) {
143+
return $media_thumbnail[0]['attribs']['']['url'];
144+
}
145+
146+
// Method 2: Try media:content.
147+
$media_content = $item->get_item_tags( 'http://search.yahoo.com/mrss/', 'content' );
148+
if ( $media_content && isset( $media_content[0]['attribs']['']['url'] ) ) {
149+
return $media_content[0]['attribs']['']['url'];
150+
}
151+
152+
// Method 3: Try enclosure.
153+
$enclosure = $item->get_enclosure();
154+
if ( $enclosure ) {
155+
if ( $enclosure->get_thumbnail() ) {
156+
return $enclosure->get_thumbnail();
157+
}
158+
if ( $enclosure->get_link() && \str_contains( $enclosure->get_type(), 'image' ) ) {
159+
return $enclosure->get_link();
160+
}
161+
}
162+
163+
// Method 4: Try to extract from description HTML (prioritize featured images).
164+
$description = $item->get_description();
165+
if ( $description ) {
166+
$image_url = $this->extract_image_from_html( $description, true );
167+
if ( $image_url ) {
168+
return $image_url;
169+
}
170+
}
171+
172+
// Method 5: Try to extract from content HTML (prioritize featured images).
173+
$content = $item->get_content();
174+
if ( $content ) {
175+
$image_url = $this->extract_image_from_html( $content, true );
176+
if ( $image_url ) {
177+
return $image_url;
178+
}
179+
}
180+
181+
// Method 6: Try to get from REST API as fallback if feed is from a WordPress site.
182+
$permalink = $item->get_permalink();
183+
if ( $permalink && \str_contains( $permalink, \progress_planner()->get_ui__branding()->get_blog_feed_url() ) ) {
184+
$rest_api_image = $this->get_featured_image_from_rest_api( $permalink );
185+
if ( $rest_api_image ) {
186+
return $rest_api_image;
187+
}
188+
}
189+
190+
// Method 7: Try to extract ANY image from content as last resort.
191+
if ( $content ) {
192+
$image_url = $this->extract_image_from_html( $content, false );
193+
if ( $image_url ) {
194+
return $image_url;
195+
}
196+
}
197+
198+
// Method 8: Try to extract ANY image from description as last resort.
199+
if ( $description ) {
200+
$image_url = $this->extract_image_from_html( $description, false );
201+
if ( $image_url ) {
202+
return $image_url;
203+
}
204+
}
205+
206+
return null;
207+
}
208+
209+
/**
210+
* Extract image URL from HTML content.
211+
*
212+
* Checks for various HTML elements and attributes that can contain images:
213+
* - <img> tags (src, srcset, data-src)
214+
* - <picture> elements (source tags and img fallback)
215+
* - <figure> elements
216+
* - Background images in style attributes
217+
*
218+
* @param string $html The HTML content to parse.
219+
* @param bool $prioritize_featured Whether to prioritize featured images (wp-post-thumbnail, wp-post-image).
220+
* @return string|null The first image URL found, or null if none found.
221+
*/
222+
private function extract_image_from_html( $html, $prioritize_featured = false ) {
223+
if ( empty( $html ) ) {
224+
return null;
225+
}
226+
227+
// If prioritizing featured images, check for WordPress featured image classes first.
228+
if ( $prioritize_featured ) {
229+
// Check for wp-post-thumbnail or wp-post-image class.
230+
if ( \preg_match( '/<img[^>]+class=["\'][^"\']*(?:wp-post-thumbnail|wp-post-image)[^"\']*["\'][^>]+src=["\']([^"\']+)["\'][^>]*>/i', $html, $matches ) ) {
231+
return $matches[1];
232+
}
233+
if ( \preg_match( '/<img[^>]+src=["\']([^"\']+)["\'][^>]+class=["\'][^"\']*(?:wp-post-thumbnail|wp-post-image)[^"\']*["\'][^>]*>/i', $html, $matches ) ) {
234+
return $matches[1];
235+
}
236+
}
237+
238+
// Check for <picture> element with <source> tags.
239+
if ( \preg_match( '/<picture[^>]*>.*?<source[^>]+srcset=["\']([^"\'\s]+)[^"\']*["\'][^>]*>.*?<\/picture>/is', $html, $matches ) ) {
240+
// Extract first URL from srcset (may contain multiple URLs with sizes).
241+
$srcset = $matches[1];
242+
if ( \preg_match( '/^([^\s]+)/', $srcset, $url_match ) ) {
243+
return $url_match[1];
244+
}
245+
}
246+
247+
// Check for <img> with srcset attribute (responsive images).
248+
if ( \preg_match( '/<img[^>]+srcset=["\']([^"\'\s]+)[^"\']*["\'][^>]*>/i', $html, $matches ) ) {
249+
// Extract first URL from srcset.
250+
$srcset = $matches[1];
251+
if ( \preg_match( '/^([^\s]+)/', $srcset, $url_match ) ) {
252+
return $url_match[1];
253+
}
254+
}
255+
256+
// Check for lazy-loaded images (data-src).
257+
if ( \preg_match( '/<img[^>]+data-src=["\']([^"\']+)["\'][^>]*>/i', $html, $matches ) ) {
258+
return $matches[1];
259+
}
260+
261+
// Check for standard <img> src.
262+
if ( \preg_match( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $html, $matches ) ) {
263+
return $matches[1];
264+
}
265+
266+
// Check for background images in style attributes.
267+
if ( \preg_match( '/style=["\'][^"\']*background-image\s*:\s*url\(["\']?([^"\')]+)["\']?\)[^"\']*["\'][^>]*>/i', $html, $matches ) ) {
268+
return $matches[1];
269+
}
270+
271+
// Check for <figure> with nested img.
272+
if ( \preg_match( '/<figure[^>]*>.*?<img[^>]+src=["\']([^"\']+)["\'][^>]*>.*?<\/figure>/is', $html, $matches ) ) {
273+
return $matches[1];
274+
}
275+
276+
return null;
277+
}
278+
279+
/**
280+
* Get featured image from WordPress REST API using post permalink.
281+
*
282+
* @param string $permalink The post permalink.
283+
* @return string|null The featured image URL or null if not found.
284+
*/
285+
private function get_featured_image_from_rest_api( $permalink ) {
286+
// Extract post slug from permalink.
287+
$slug = \basename( \untrailingslashit( $permalink ) );
288+
if ( ! $slug ) {
289+
return null;
290+
}
291+
292+
// Try to get post data from REST API.
293+
$api_url = \trailingslashit( \progress_planner()->get_ui__branding()->get_blog_feed_url() ) . 'wp-json/wp/v2/posts?slug=' . $slug;
294+
$response = \wp_remote_get( $api_url );
295+
296+
if ( \is_wp_error( $response ) || 200 !== \wp_remote_retrieve_response_code( $response ) ) {
297+
return null;
298+
}
299+
300+
$posts = \json_decode( \wp_remote_retrieve_body( $response ), true );
301+
if ( empty( $posts ) || ! isset( $posts[0]['featured_media'] ) || ! $posts[0]['featured_media'] ) {
302+
return null;
303+
}
304+
305+
// Get the featured media.
306+
$media_id = $posts[0]['featured_media'];
307+
$media_response = \wp_remote_get(
308+
\trailingslashit( \progress_planner()->get_ui__branding()->get_blog_feed_url() ) . 'wp-json/wp/v2/media/' . $media_id
309+
);
310+
311+
if ( \is_wp_error( $media_response ) || 200 !== \wp_remote_retrieve_response_code( $media_response ) ) {
312+
return null;
313+
}
314+
315+
$media = \json_decode( \wp_remote_retrieve_body( $media_response ), true );
316+
if ( isset( $media['media_details']['sizes']['medium_large']['source_url'] ) ) {
317+
return $media['media_details']['sizes']['medium_large']['source_url'];
318+
}
319+
320+
if ( isset( $media['source_url'] ) ) {
321+
return $media['source_url'];
322+
}
323+
324+
return null;
325+
}
326+
84327
/**
85328
* Get the cache key.
86329
*

0 commit comments

Comments
 (0)