103 |
103 |
104 unset( $shortcode_tags[ $tag ] ); |
104 unset( $shortcode_tags[ $tag ] ); |
105 } |
105 } |
106 |
106 |
107 /** |
107 /** |
108 * Clear all shortcodes. |
108 * Clears all shortcodes. |
109 * |
109 * |
110 * This function is simple, it clears all of the shortcode tags by replacing the |
110 * This function clears all of the shortcode tags by replacing the shortcodes global with |
111 * shortcodes global by a empty array. This is actually a very efficient method |
111 * an empty array. This is actually an efficient method for removing all shortcodes. |
112 * for removing all shortcodes. |
|
113 * |
112 * |
114 * @since 2.5.0 |
113 * @since 2.5.0 |
115 * |
114 * |
116 * @global array $shortcode_tags |
115 * @global array $shortcode_tags |
117 */ |
116 */ |
135 global $shortcode_tags; |
134 global $shortcode_tags; |
136 return array_key_exists( $tag, $shortcode_tags ); |
135 return array_key_exists( $tag, $shortcode_tags ); |
137 } |
136 } |
138 |
137 |
139 /** |
138 /** |
140 * Whether the passed content contains the specified shortcode |
139 * Determines whether the passed content contains the specified shortcode. |
141 * |
140 * |
142 * @since 3.6.0 |
141 * @since 3.6.0 |
143 * |
142 * |
144 * @global array $shortcode_tags |
143 * @global array $shortcode_tags |
145 * |
144 * |
146 * @param string $content Content to search for shortcodes. |
145 * @param string $content Content to search for shortcodes. |
147 * @param string $tag Shortcode tag to check. |
146 * @param string $tag Shortcode tag to check. |
148 * @return bool Whether the passed content contains the given shortcode. |
147 * @return bool Whether the passed content contains the given shortcode. |
149 */ |
148 */ |
150 function has_shortcode( $content, $tag ) { |
149 function has_shortcode( $content, $tag ) { |
151 if ( false === strpos( $content, '[' ) ) { |
150 if ( ! str_contains( $content, '[' ) ) { |
152 return false; |
151 return false; |
153 } |
152 } |
154 |
153 |
155 if ( shortcode_exists( $tag ) ) { |
154 if ( shortcode_exists( $tag ) ) { |
156 preg_match_all( '/' . get_shortcode_regex() . '/', $content, $matches, PREG_SET_ORDER ); |
155 preg_match_all( '/' . get_shortcode_regex() . '/', $content, $matches, PREG_SET_ORDER ); |
168 } |
167 } |
169 return false; |
168 return false; |
170 } |
169 } |
171 |
170 |
172 /** |
171 /** |
173 * Search content for shortcodes and filter shortcodes through their hooks. |
172 * Returns a list of registered shortcode names found in the given content. |
|
173 * |
|
174 * Example usage: |
|
175 * |
|
176 * get_shortcode_tags_in_content( '[audio src="file.mp3"][/audio] [foo] [gallery ids="1,2,3"]' ); |
|
177 * // array( 'audio', 'gallery' ) |
|
178 * |
|
179 * @since 6.3.2 |
|
180 * |
|
181 * @param string $content The content to check. |
|
182 * @return string[] An array of registered shortcode names found in the content. |
|
183 */ |
|
184 function get_shortcode_tags_in_content( $content ) { |
|
185 if ( false === strpos( $content, '[' ) ) { |
|
186 return array(); |
|
187 } |
|
188 |
|
189 preg_match_all( '/' . get_shortcode_regex() . '/', $content, $matches, PREG_SET_ORDER ); |
|
190 if ( empty( $matches ) ) { |
|
191 return array(); |
|
192 } |
|
193 |
|
194 $tags = array(); |
|
195 foreach ( $matches as $shortcode ) { |
|
196 $tags[] = $shortcode[2]; |
|
197 |
|
198 if ( ! empty( $shortcode[5] ) ) { |
|
199 $deep_tags = get_shortcode_tags_in_content( $shortcode[5] ); |
|
200 if ( ! empty( $deep_tags ) ) { |
|
201 $tags = array_merge( $tags, $deep_tags ); |
|
202 } |
|
203 } |
|
204 } |
|
205 |
|
206 return $tags; |
|
207 } |
|
208 |
|
209 /** |
|
210 * Searches content for shortcodes and filter shortcodes through their hooks. |
174 * |
211 * |
175 * This function is an alias for do_shortcode(). |
212 * This function is an alias for do_shortcode(). |
176 * |
213 * |
177 * @since 5.4.0 |
214 * @since 5.4.0 |
178 * |
215 * |
186 function apply_shortcodes( $content, $ignore_html = false ) { |
223 function apply_shortcodes( $content, $ignore_html = false ) { |
187 return do_shortcode( $content, $ignore_html ); |
224 return do_shortcode( $content, $ignore_html ); |
188 } |
225 } |
189 |
226 |
190 /** |
227 /** |
191 * Search content for shortcodes and filter shortcodes through their hooks. |
228 * Searches content for shortcodes and filter shortcodes through their hooks. |
192 * |
229 * |
193 * If there are no shortcode tags defined, then the content will be returned |
230 * If there are no shortcode tags defined, then the content will be returned |
194 * without any filtering. This might cause issues when plugins are disabled but |
231 * without any filtering. This might cause issues when plugins are disabled but |
195 * the shortcode will still show up in the post or content. |
232 * the shortcode will still show up in the post or content. |
196 * |
233 * |
204 * @return string Content with shortcodes filtered out. |
241 * @return string Content with shortcodes filtered out. |
205 */ |
242 */ |
206 function do_shortcode( $content, $ignore_html = false ) { |
243 function do_shortcode( $content, $ignore_html = false ) { |
207 global $shortcode_tags; |
244 global $shortcode_tags; |
208 |
245 |
209 if ( false === strpos( $content, '[' ) ) { |
246 if ( ! str_contains( $content, '[' ) ) { |
210 return $content; |
247 return $content; |
211 } |
248 } |
212 |
249 |
213 if ( empty( $shortcode_tags ) || ! is_array( $shortcode_tags ) ) { |
250 if ( empty( $shortcode_tags ) || ! is_array( $shortcode_tags ) ) { |
214 return $content; |
251 return $content; |
220 |
257 |
221 if ( empty( $tagnames ) ) { |
258 if ( empty( $tagnames ) ) { |
222 return $content; |
259 return $content; |
223 } |
260 } |
224 |
261 |
|
262 // Ensure this context is only added once if shortcodes are nested. |
|
263 $has_filter = has_filter( 'wp_get_attachment_image_context', '_filter_do_shortcode_context' ); |
|
264 $filter_added = false; |
|
265 |
|
266 if ( ! $has_filter ) { |
|
267 $filter_added = add_filter( 'wp_get_attachment_image_context', '_filter_do_shortcode_context' ); |
|
268 } |
|
269 |
225 $content = do_shortcodes_in_html_tags( $content, $ignore_html, $tagnames ); |
270 $content = do_shortcodes_in_html_tags( $content, $ignore_html, $tagnames ); |
226 |
271 |
227 $pattern = get_shortcode_regex( $tagnames ); |
272 $pattern = get_shortcode_regex( $tagnames ); |
228 $content = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $content ); |
273 $content = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $content ); |
229 |
274 |
230 // Always restore square braces so we don't break things like <!--[if IE ]>. |
275 // Always restore square braces so we don't break things like <!--[if IE ]>. |
231 $content = unescape_invalid_shortcodes( $content ); |
276 $content = unescape_invalid_shortcodes( $content ); |
232 |
277 |
|
278 // Only remove the filter if it was added in this scope. |
|
279 if ( $filter_added ) { |
|
280 remove_filter( 'wp_get_attachment_image_context', '_filter_do_shortcode_context' ); |
|
281 } |
|
282 |
233 return $content; |
283 return $content; |
234 } |
284 } |
235 |
285 |
236 /** |
286 /** |
237 * Retrieve the shortcode regular expression for searching. |
287 * Filter the `wp_get_attachment_image_context` hook during shortcode rendering. |
|
288 * |
|
289 * When wp_get_attachment_image() is called during shortcode rendering, we need to make clear |
|
290 * that the context is a shortcode and not part of the theme's template rendering logic. |
|
291 * |
|
292 * @since 6.3.0 |
|
293 * @access private |
|
294 * |
|
295 * @return string The filtered context value for wp_get_attachment_images when doing shortcodes. |
|
296 */ |
|
297 function _filter_do_shortcode_context() { |
|
298 return 'do_shortcode'; |
|
299 } |
|
300 |
|
301 /** |
|
302 * Retrieves the shortcode regular expression for searching. |
238 * |
303 * |
239 * The regular expression combines the shortcode tags in the regular expression |
304 * The regular expression combines the shortcode tags in the regular expression |
240 * in a regex class. |
305 * in a regex class. |
241 * |
306 * |
242 * The regular expression contains 6 different sub matches to help with parsing. |
307 * The regular expression contains 6 different sub matches to help with parsing. |
262 if ( empty( $tagnames ) ) { |
327 if ( empty( $tagnames ) ) { |
263 $tagnames = array_keys( $shortcode_tags ); |
328 $tagnames = array_keys( $shortcode_tags ); |
264 } |
329 } |
265 $tagregexp = implode( '|', array_map( 'preg_quote', $tagnames ) ); |
330 $tagregexp = implode( '|', array_map( 'preg_quote', $tagnames ) ); |
266 |
331 |
267 // WARNING! Do not change this regex without changing do_shortcode_tag() and strip_shortcode_tag(). |
332 /* |
268 // Also, see shortcode_unautop() and shortcode.js. |
333 * WARNING! Do not change this regex without changing do_shortcode_tag() and strip_shortcode_tag(). |
|
334 * Also, see shortcode_unautop() and shortcode.js. |
|
335 */ |
269 |
336 |
270 // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- don't remove regex indentation |
337 // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- don't remove regex indentation |
271 return '\\[' // Opening bracket. |
338 return '\\[' // Opening bracket. |
272 . '(\\[?)' // 1: Optional second opening bracket for escaping shortcodes: [[tag]]. |
339 . '(\\[?)' // 1: Optional second opening bracket for escaping shortcodes: [[tag]]. |
273 . "($tagregexp)" // 2: Shortcode name. |
340 . "($tagregexp)" // 2: Shortcode name. |
307 * @since 2.5.0 |
374 * @since 2.5.0 |
308 * @access private |
375 * @access private |
309 * |
376 * |
310 * @global array $shortcode_tags |
377 * @global array $shortcode_tags |
311 * |
378 * |
312 * @param array $m Regular expression match array. |
379 * @param array $m { |
313 * @return string|false Shortcode output on success, false on failure. |
380 * Regular expression match array. |
|
381 * |
|
382 * @type string $0 Entire matched shortcode text. |
|
383 * @type string $1 Optional second opening bracket for escaping shortcodes. |
|
384 * @type string $2 Shortcode name. |
|
385 * @type string $3 Shortcode arguments list. |
|
386 * @type string $4 Optional self closing slash. |
|
387 * @type string $5 Content of a shortcode when it wraps some content. |
|
388 * @type string $6 Optional second closing bracket for escaping shortcodes. |
|
389 * } |
|
390 * @return string Shortcode output. |
314 */ |
391 */ |
315 function do_shortcode_tag( $m ) { |
392 function do_shortcode_tag( $m ) { |
316 global $shortcode_tags; |
393 global $shortcode_tags; |
317 |
394 |
318 // Allow [[foo]] syntax for escaping a tag. |
395 // Allow [[foo]] syntax for escaping a tag. |
338 * |
415 * |
339 * Returning a non-false value from filter will short-circuit the |
416 * Returning a non-false value from filter will short-circuit the |
340 * shortcode generation process, returning that value instead. |
417 * shortcode generation process, returning that value instead. |
341 * |
418 * |
342 * @since 4.7.0 |
419 * @since 4.7.0 |
|
420 * @since 6.5.0 The `$attr` parameter is always an array. |
343 * |
421 * |
344 * @param false|string $return Short-circuit return value. Either false or the value to replace the shortcode with. |
422 * @param false|string $output Short-circuit return value. Either false or the value to replace the shortcode with. |
345 * @param string $tag Shortcode name. |
423 * @param string $tag Shortcode name. |
346 * @param array|string $attr Shortcode attributes array or empty string. |
424 * @param array $attr Shortcode attributes array, can be empty if the original arguments string cannot be parsed. |
347 * @param array $m Regular expression match array. |
425 * @param array $m Regular expression match array. |
348 */ |
426 */ |
349 $return = apply_filters( 'pre_do_shortcode_tag', false, $tag, $attr, $m ); |
427 $return = apply_filters( 'pre_do_shortcode_tag', false, $tag, $attr, $m ); |
350 if ( false !== $return ) { |
428 if ( false !== $return ) { |
351 return $return; |
429 return $return; |
352 } |
430 } |
357 |
435 |
358 /** |
436 /** |
359 * Filters the output created by a shortcode callback. |
437 * Filters the output created by a shortcode callback. |
360 * |
438 * |
361 * @since 4.7.0 |
439 * @since 4.7.0 |
|
440 * @since 6.5.0 The `$attr` parameter is always an array. |
362 * |
441 * |
363 * @param string $output Shortcode output. |
442 * @param string $output Shortcode output. |
364 * @param string $tag Shortcode name. |
443 * @param string $tag Shortcode name. |
365 * @param array|string $attr Shortcode attributes array or empty string. |
444 * @param array $attr Shortcode attributes array, can be empty if the original arguments string cannot be parsed. |
366 * @param array $m Regular expression match array. |
445 * @param array $m Regular expression match array. |
367 */ |
446 */ |
368 return apply_filters( 'do_shortcode_tag', $output, $tag, $attr, $m ); |
447 return apply_filters( 'do_shortcode_tag', $output, $tag, $attr, $m ); |
369 } |
448 } |
370 |
449 |
371 /** |
450 /** |
372 * Search only inside HTML elements for shortcodes and process them. |
451 * Searches only inside HTML elements for shortcodes and process them. |
373 * |
452 * |
374 * Any [ or ] characters remaining inside elements will be HTML encoded |
453 * Any [ or ] characters remaining inside elements will be HTML encoded |
375 * to prevent interference with shortcodes that are outside the elements. |
454 * to prevent interference with shortcodes that are outside the elements. |
376 * Assumes $content processed by KSES already. Users with unfiltered_html |
455 * Assumes $content processed by KSES already. Users with unfiltered_html |
377 * capability may get unexpected output if angle braces are nested in tags. |
456 * capability may get unexpected output if angle braces are nested in tags. |
401 foreach ( $textarr as &$element ) { |
480 foreach ( $textarr as &$element ) { |
402 if ( '' === $element || '<' !== $element[0] ) { |
481 if ( '' === $element || '<' !== $element[0] ) { |
403 continue; |
482 continue; |
404 } |
483 } |
405 |
484 |
406 $noopen = false === strpos( $element, '[' ); |
485 $noopen = ! str_contains( $element, '[' ); |
407 $noclose = false === strpos( $element, ']' ); |
486 $noclose = ! str_contains( $element, ']' ); |
408 if ( $noopen || $noclose ) { |
487 if ( $noopen || $noclose ) { |
409 // This element does not contain shortcodes. |
488 // This element does not contain shortcodes. |
410 if ( $noopen xor $noclose ) { |
489 if ( $noopen xor $noclose ) { |
411 // Need to encode stray '[' or ']' chars. |
490 // Need to encode stray '[' or ']' chars. |
412 $element = strtr( $element, $trans ); |
491 $element = strtr( $element, $trans ); |
413 } |
492 } |
414 continue; |
493 continue; |
415 } |
494 } |
416 |
495 |
417 if ( $ignore_html || '<!--' === substr( $element, 0, 4 ) || '<![CDATA[' === substr( $element, 0, 9 ) ) { |
496 if ( $ignore_html || str_starts_with( $element, '<!--' ) || str_starts_with( $element, '<![CDATA[' ) ) { |
418 // Encode all '[' and ']' chars. |
497 // Encode all '[' and ']' chars. |
419 $element = strtr( $element, $trans ); |
498 $element = strtr( $element, $trans ); |
420 continue; |
499 continue; |
421 } |
500 } |
422 |
501 |
425 // Some plugins are doing things like [name] <[email]>. |
504 // Some plugins are doing things like [name] <[email]>. |
426 if ( 1 === preg_match( '%^<\s*\[\[?[^\[\]]+\]%', $element ) ) { |
505 if ( 1 === preg_match( '%^<\s*\[\[?[^\[\]]+\]%', $element ) ) { |
427 $element = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $element ); |
506 $element = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $element ); |
428 } |
507 } |
429 |
508 |
430 // Looks like we found some crazy unfiltered HTML. Skipping it for sanity. |
509 // Looks like we found some unexpected unfiltered HTML. Skipping it for confidence. |
431 $element = strtr( $element, $trans ); |
510 $element = strtr( $element, $trans ); |
432 continue; |
511 continue; |
433 } |
512 } |
434 |
513 |
435 // Get element name. |
514 // Get element name. |
455 * was written by an administrator, so we should avoid changing the output |
534 * was written by an administrator, so we should avoid changing the output |
456 * and we do not need to run KSES here. |
535 * and we do not need to run KSES here. |
457 */ |
536 */ |
458 $attr = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $attr ); |
537 $attr = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $attr ); |
459 } else { |
538 } else { |
460 // $attr like 'name = "[shortcode]"' or "name = '[shortcode]'". |
539 /* |
461 // We do not know if $content was unfiltered. Assume KSES ran before shortcodes. |
540 * $attr like 'name = "[shortcode]"' or "name = '[shortcode]'". |
|
541 * We do not know if $content was unfiltered. Assume KSES ran before shortcodes. |
|
542 */ |
462 $count = 0; |
543 $count = 0; |
463 $new_attr = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $attr, -1, $count ); |
544 $new_attr = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $attr, -1, $count ); |
464 if ( $count > 0 ) { |
545 if ( $count > 0 ) { |
465 // Sanitize the shortcode output using KSES. |
546 // Sanitize the shortcode output using KSES. |
466 $new_attr = wp_kses_one_attr( $new_attr, $elname ); |
547 $new_attr = wp_kses_one_attr( $new_attr, $elname ); |
501 |
582 |
502 return $content; |
583 return $content; |
503 } |
584 } |
504 |
585 |
505 /** |
586 /** |
506 * Retrieve the shortcode attributes regex. |
587 * Retrieves the shortcode attributes regex. |
507 * |
588 * |
508 * @since 4.4.0 |
589 * @since 4.4.0 |
509 * |
590 * |
510 * @return string The shortcode attribute regular expression |
591 * @return string The shortcode attribute regular expression. |
511 */ |
592 */ |
512 function get_shortcode_atts_regex() { |
593 function get_shortcode_atts_regex() { |
513 return '/([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)|([\w-]+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|\'([^\']*)\'(?:\s|$)|(\S+)(?:\s|$)/'; |
594 return '/([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)|([\w-]+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|\'([^\']*)\'(?:\s|$)|(\S+)(?:\s|$)/'; |
514 } |
595 } |
515 |
596 |
516 /** |
597 /** |
517 * Retrieve all attributes from the shortcodes tag. |
598 * Retrieves all attributes from the shortcodes tag. |
518 * |
599 * |
519 * The attributes list has the attribute name as the key and the value of the |
600 * The attributes list has the attribute name as the key and the value of the |
520 * attribute as the value in the key/value pair. This allows for easier |
601 * attribute as the value in the key/value pair. This allows for easier |
521 * retrieval of the attributes, since all attributes have to be known. |
602 * retrieval of the attributes, since all attributes have to be known. |
522 * |
603 * |
523 * @since 2.5.0 |
604 * @since 2.5.0 |
524 * |
605 * @since 6.5.0 The function now always returns an array, |
525 * @param string $text |
606 * even if the original arguments string cannot be parsed or is empty. |
526 * @return array|string List of attribute values. |
607 * |
527 * Returns empty array if '""' === trim( $text ). |
608 * @param string $text Shortcode arguments list. |
528 * Returns empty string if '' === trim( $text ). |
609 * @return array Array of attribute values keyed by attribute name. |
529 * All other matches are checked for not empty(). |
610 * Returns empty array if there are no attributes |
|
611 * or if the original arguments string cannot be parsed. |
530 */ |
612 */ |
531 function shortcode_parse_atts( $text ) { |
613 function shortcode_parse_atts( $text ) { |
532 $atts = array(); |
614 $atts = array(); |
533 $pattern = get_shortcode_atts_regex(); |
615 $pattern = get_shortcode_atts_regex(); |
534 $text = preg_replace( "/[\x{00a0}\x{200b}]+/u", ' ', $text ); |
616 $text = preg_replace( "/[\x{00a0}\x{200b}]+/u", ' ', $text ); |
549 } |
631 } |
550 } |
632 } |
551 |
633 |
552 // Reject any unclosed HTML elements. |
634 // Reject any unclosed HTML elements. |
553 foreach ( $atts as &$value ) { |
635 foreach ( $atts as &$value ) { |
554 if ( false !== strpos( $value, '<' ) ) { |
636 if ( str_contains( $value, '<' ) ) { |
555 if ( 1 !== preg_match( '/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', $value ) ) { |
637 if ( 1 !== preg_match( '/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', $value ) ) { |
556 $value = ''; |
638 $value = ''; |
557 } |
639 } |
558 } |
640 } |
559 } |
641 } |
560 } else { |
|
561 $atts = ltrim( $text ); |
|
562 } |
642 } |
563 |
643 |
564 return $atts; |
644 return $atts; |
565 } |
645 } |
566 |
646 |
567 /** |
647 /** |
568 * Combine user attributes with known attributes and fill in defaults when needed. |
648 * Combines user attributes with known attributes and fill in defaults when needed. |
569 * |
649 * |
570 * The pairs should be considered to be all of the attributes which are |
650 * The pairs should be considered to be all of the attributes which are |
571 * supported by the caller and given as a list. The returned attributes will |
651 * supported by the caller and given as a list. The returned attributes will |
572 * only contain the attributes in the $pairs list. |
652 * only contain the attributes in the $pairs list. |
573 * |
653 * |
624 * @return string Content without shortcode tags. |
704 * @return string Content without shortcode tags. |
625 */ |
705 */ |
626 function strip_shortcodes( $content ) { |
706 function strip_shortcodes( $content ) { |
627 global $shortcode_tags; |
707 global $shortcode_tags; |
628 |
708 |
629 if ( false === strpos( $content, '[' ) ) { |
709 if ( ! str_contains( $content, '[' ) ) { |
630 return $content; |
710 return $content; |
631 } |
711 } |
632 |
712 |
633 if ( empty( $shortcode_tags ) || ! is_array( $shortcode_tags ) ) { |
713 if ( empty( $shortcode_tags ) || ! is_array( $shortcode_tags ) ) { |
634 return $content; |
714 return $content; |